0%

SpringCloud

SpringCloud

SpringCloud

描述:一套完整的微服务解决方案,一系列不同功能的微服务框架的集合。

类别:Netflix(奈飞),Alibaba

源码:https://github.com/qiyisoft/sca

各个组件

  1. Nacos
  2. Ribbon
  3. OpenFeign
  4. GateWay
  5. Sentinel
  6. SkyWalking
  7. Seata
  8. 分布式锁
  9. 认证与授权 OAuth2+JWT+Security

1.注册中心Nacos

特性,Nacos使用,Nacos的心跳机制和健康检查

特性

出现的背景:在以往单实例情况下,服务间通常采用点对点通信,即采用 IP+端口+接口的形式直接调用。考虑避免单点负载压力过大以及高可用的性能要求,通常会部署多实例节点保障系统的性能,但增加多实例后,调用方该如何选择哪个服务提供者进行处理呢?还有当服务提供者出现故障后,如何将后续请求转移到其他可用实例上呢?

功能:

  • 服务发现与管理

  • 动态配置服务

  • 动态DNS服务

Nacos注册中心使用

服务端部署
  1. 环境准备

    1. 操作系统CentOS7

    2. 安装JDK8

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      yum -y install java-1.8.0-openjdk-devel.x86_64 # 默认安装位置: /usr/lib/jvm/
      # 安装成功后验证Java版本
      java -version

      # 编辑profile配置 JAVA_HOME 环境变量
      [root@server-1 ~]# vim /etc/profile
      export JAVA_HOME=/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.272.b10-1.el7_9.x86_64
      export JRE_HOME=$JAVA_HOME/jre
      export CLASSPATH=$JAVA_HOME/lib:$JRE_HOME/lib:$CLASSPATH
      export PATH=$JAVA_HOME/bin:$JRE_HOME/bin:$PATH
      # source:在当前bash环境下读取并执行FileName中的命令。
      [root@server-1 ~]# source /etc/profile

      # 验证配置是否正确 ? java javac
      [root@server-1 ~]# echo $JAVA_HOME
      /usr/lib/jvm/java-1.8.0-openjdk-1.8.0.272.b10-1.el7_9.x86_64
  2. 安装并启动

    1. 获取安装包

      访问 Nacos GitHub:https://github.com/alibaba/nacos/releases/获取 Nacos 安装包 nacos-server-1.4.0.tar.gz。

    2. 上传&解压缩

      1
      [root@server-1 local]#  tar -xvf nacos-server-1.4.0.tar.gz
    3. 启动

      1
      2
      3
      [root@server-1 local]# cd nacos/bin
      # 以单点方式启动 Nacos
      [root@server-1 bin]# sh startup.sh -m standalone

      Nacos默认以后台模式启动,利用 tail 命令查看启动日志。

      1
      2
      3
      4
      5
      6
      [root@server-1 bin]# tail -f /usr/local/nacos/logs/start.out
      2020-12-06 21:03:18,759 INFO Tomcat started on port(s): 8848 (http) with context path '/nacos'
      2020-12-06 21:03:18,766 INFO Nacos Log files: /usr/local/nacos/nacos/logs
      2020-12-06 21:03:18,766 INFO Nacos Log files: /usr/loca/nacos/nacos/conf
      2020-12-06 21:03:18,766 INFO Nacos Log files: /usr/local/nacos/nacos/data
      2020-12-06 21:03:18,767 INFO Nacos started successfully in stand alone mode. use embedded storage
    4. 对外开放7848/8848端口

      8848 端口是 Nacos 对客户端提供服务的端口,7848 是 Nacos 集群通信端口,用于Nacos 集群间进行选举,检测等。

      1
      2
      3
      4
      5
      6
      [root@server-1 bin]# firewall-cmd --zone=public --add-port=8848/tcp --permanent
      success
      [root@server-1 bin]# firewall-cmd --zone=public --add-port=7848/tcp --permanent
      success
      [root@server-1 bin]# firewall-cmd --reload
      success
    5. 从浏览器进入管理界面

      http://192.168.31.102:8848/nacos

      账号&密码 nacos&nacos

      服务管理->服务列表,用于查看已注册微服务列表。

      image-20210704204632363

微服务接入nacos
  1. 创建项目

    idea工具,创建项目,选择Spring InitiaLizr

    Custom可以选择阿里云镜像地址 http://start.aliyun.com

    image-20210705063904157

  2. 添加依赖

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    <!-- nacos依赖 -->
    <dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>

    <!-- web 使微服务具备http相应能力 -->
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
  3. 配置nacos相关属性

    1
    2
    3
    4
    5
    6
    7
    8
    9
    # 应用名称,默认也是在微服务中注册的微服务 ID
    spring.application.name=sample-service
    # 配置 Nacos 服务器的IP地址
    spring.cloud.nacos.discovery.server-addr=192.168.31.102:8848
    #连接 Nacos 服务器使用的用户名、密码,默认为 nacos
    spring.cloud.nacos.discovery.username=nacos
    spring.cloud.nacos.discvery.password=nacos
    #微服务提供Web服务的端口号
    server.port=8081
  4. 启动项目

    启动日志

    1
    2
    3
    4
    5
    6
    #Web 服务端口号 8081
    INFO 14188 o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8081 (http) with context path ''
    #微服务向 Nacos 注册成功,微服务 ID:sample-service
    INFO 14188 c.a.c.n.registry.NacosServiceRegistry : nacos registry, DEFAULT_GROUP sample-service 192.168.47.1:8081 register finished
    #微服务启动成功
    INFO 14188 c.l.s.SampleServiceApplication : Started SampleServiceApplication in 4.911 seconds (JVM running for 6.039)
  5. nacos查看刚刚注册的项目

    浏览器打开 http://192.168.31.102:8848/nacos ,服务管理-服务列表

    image-20210705065001740

Nacos配置中心

介绍

场景:

  • 动态配置,无需重启服务
  • 多实例批量修改
  • 版本管理
  • 配置文件多环境切换
  • 变更推送
  • 监听查询?

微服务应用只持有应用启动的最小化配置,在应用启动时微服务应用所需的其他配置数据,诸如数据库连接字符串、各种用户名密码、IP 等信息均从配置中心远程下载。书写应用配置时不直接写入 application.yml 配置,而是直接在配置中心提供的 UI 进行设置。

使用
部署配置中心
  1. 下载Nacos

    1
    tar -xvf nacos-server-1.4.0.tar.gz
  2. 配置数据库,执行/nacos/conf/nacos-mysql.sql

    image-20210715174758339

  3. 配置Nacos数据源

    打开 /usr/local/nacos/conf/application.properties,36行

    1
    2
    3
    4
    5
    6
    ### Count of DB: 数据库总数
    db.num=1
    ### Connect URL of DB: 数据库连接,根据你的实际情况调整
    db.url.0=jdbc:mysql://192.168.31.10:3306/nacos_config?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useUnicode=true&useSSL=false&serverTimezone=UTC
    db.user=root
    db.password=root
  4. 添加所有 Nacos 集群节点 IP 及端口

    1
    2
    3
    4
    5
    # 复制命令创建 cluster.conf 文件
    cp cluster.conf.example cluster.conf
    # 打开 cluster.conf,添加所有 Nacos 集群节点 IP 及端口
    vim cluster.conf
    192.168.31.10:8848
  5. 按集群模式启动 Nacos

    1
    sh /usr/local/nacos/bin/startup.sh
  6. 访问配置页面

    http://192.168.31.10:8848/nacos/#/configurationManagement

微服务接入配置中心
  1. 引入依赖

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    <!-- Spring Boot Web模块 -->
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!-- Nacos注册中心starter -->
    <dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>
    <!-- Nacos配置中心starter -->
    <dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
    </dependency>
  2. 配置文件

    bootstrap.yml

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    # “微服务 id”-“环境名”.“文件扩展名” 三部分组合为有效的 data id,即order-service-dev.yml。data id 要和 Nacos 的设置大小写保持完全一致,这样在微服务启动时便自动会从 Nacos配置中心获取 order-service-dev.yml 配置并下载到本地完成启动过程。
    spring:
    application:
    name: order-service #微服务id
    profiles:
    active: dev #环境名
    cloud:
    nacos:
    config: #Nacos配置中心配置
    file-extension: yml #文件扩展名
    server-addr: 192.168.31.10:8848
    username: nacos
    password: nacos
    logging: #开启debug日志,本地使用
    level:
    root: debug
  3. controller

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    @RestController
    public class TestController {
    @Value("${custom.flag}")
    private String flag;
    @Value("${custom.database}")
    private String database;
    @GetMapping("/test")
    public String test(){
    return "flag:" + flag + "<br/> database:" + database;
    }
    }
  4. 配置中心配置

    image-20210716064049080

    Data ID,Group,描述,说明,配置格式,配置内容

    Data ID:配置的唯一标识,格式固定为:{微服务id}-{环境名}.yml,这里填写 order-service-dev.yml,其中 dev 就是环境名代表这个配置文件是 order-service 的开发环境配置文件。

    Group:指定配置文件的分组,这里设置默认分组 DEFAULT_GROUP 即可。

    描述:说明 order-service-dev.yml 配置文件的用途。

    配置格式:指定“配置内容”的类型,这里选择 YAML 即可。

    配置内容:

    image-20210716064151924

    点击右下角的“发布”按钮完成设置。

    image-20210716065250910

    nacos_config 数据库的 config_info 表中也出现了对应配置数据。

    image-20210716065315865

  5. 启动服务并访问接口

    1
    2
    3
    4
    http://localhost:8000/test
    结果如下:
    flag:development
    database:192.168.10.31
高级配置

####### 热加载

为了支持热加载,服务 A 的程序针对热加载需要作出如下变动:

第一,配置数据必须被封装到单独的配置 Bean 中;

第二,这个配置 Bean 需要被 @Configuration 与 @RefreshScope 两个注解描述。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Configuration // 说明这是配置Bean
@RefreshScope // 监听,当 Nacos 推送新的配置后,由这个注解负责接收并为属性重新赋值。
public class CustomConfig {
@Value("${custom.flag}")
private String flag;
@Value("${custom.database}")
private String database;
public String getFlag() {
return flag;
}
public void setFlag(String flag) {
this.flag = flag;
}
public String getDatabase() {
return database;
}
public void setDatabase(String database) {
this.database = database;
}
}

controller

1
2
3
4
5
6
7
8
9
@RestController
public class TestController {
@Resource
private CustomConfig customConfig;
@GetMapping("/test")
public String test(){
return "flag:" + customConfig.getFlag() + "<br/> database:" + customConfig.getDatabase();
}
}

启动应用后,修改配置文件,并发布.

日志立即产生重新加载的信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[192.168.31.10_8848] c.a.c.n.refresh.NacosContextRefresher    : Refresh Nacos config group=DEFAULT_GROUP,dataId=order-service-dev.yml,configInfo=server:
port: 8000
spring:
application:
name: order-service
cloud:
nacos:
discovery:
server-addr: 192.168.31.10:8848
username: nacos
password: nacos
custom: #自定义配置项
flag: development
database: 192.168.10.33
[192.168.31.10_8848] c.a.nacos.client.config.impl.CacheData : [fixed-192.168.31.10_8848] [notify-ok] dataId=order-service-dev.yml, group=DEFAULT_GROUP, md5=aeee8dceea709b3081d3c882ae2464b2, listener=com.alibaba.cloud.nacos.refresh.NacosContextRefresher$1@5bcff640

####### 切换配置文件

在 Nacos 中设置生产环境的配置,Data Id 为 order-service-prd.yml,其中 prd 是 production 的缩写,代表生产环境配置。

image-20210716163910649

调整 order-service 的 bootstrap.yml 引导文件,最重要的地方是修改环境名为 prd,同时更换为生产环境 Nacos 的通信地址,打包后发布。

1
2
3
4
5
6
7
8
9
10
11
12
spring:
application:
name: order-service #微服务id
profiles:
active: prd #环境名
cloud:
nacos:
config: #这里配置的是Nacos配置中心
file-extension: yml #指定文件扩展名
server-addr: 192.168.31.10:8848
username: nacos
password: nacos

####### 管理基础配置数据

对比 order-service-dev.yml 与 order-service-prd.yml 发现,在不同环境的配置文件中普遍存在固定的配置项,例如:spring.application.name=order-service 配置项就是稳定的,且修改它会影响所有环境配置文件。对于这种基础的全局配置,我们可以将其存放到单独的 order-service.yml 配置中,在 order-service 服务启动时,这个不带环境名的配置文件必然会被加载。

image-20210716164052286

1
2
3
4
# order-service.yml
spring:
application:
name: order-service
1
2
3
4
5
6
7
8
9
10
11
12
13
# order-service-dev.yml
server:
port: 8000
spring:
cloud:
nacos:
discovery:
server-addr: 192.168.31.10:8848
username: nacos
password: nacos
custom: #自定义配置项
flag: development
database: 192.168.10.33

Nacos原理

心跳机制与健康检查

微服务与Nacos服务器通信过程:

  1. 微服务(客户端)启动后每过5秒,会由微服务内置的 Nacos 客户端主动向 Nacos 服务器发起心跳包(HeartBeat)。心跳包会包含当前服务实例的名称、IP、端口、集群名、权重等信息。

    image-20210705065425475

  2. Nacos服务端收到心跳包后的处理

    1. 首先根据 IP 与端口判断 Nacos 是否存在该服务实例?如果实例信息不存在,在 Nacos 中注册登记该实例。而注册的本质是将新实例对象存储在“实例 Map”集合中;

    2. 如果实例信息已存在,记录本次心跳包发送时间;

    3. 设置实例状态为“健康”;

    4. 推送“微服务状态变更”消息;

    5. 返回心跳包时间间隔。

      image-20210705065743612

  3. 实例的不健康状态与剔除

    1
    2
    3
    4
    5
    6
    7
    #心跳间隔。时间单位:秒。心跳间隔
    spring.cloud.nacos.discovery.heart-beat-interval=3

    # Nacos Server 默认每过 15 秒对“实例 Map”中的所有实例进行扫描,服务端6秒收不到客户端心跳,会将该客户端注册的实例设为不健康
    spring.cloud.nacos.discovery.heart-beat-timeout=15
    # Nacos Server 默认每过 20 秒对“实例 Map”中的所有“非健康”实例进行扫描,如发现“非健康”实例,随即从“实例 Map”中将该实例删除。
    spring.cloud.nacos.discovery.ip-delete-timeout=30
热加载原理

热加载介绍:Nacos 中支持配置热加载,在运行过程中允许直接对新的配置项进行重新加载而不需要手动重启。

配置中心长轮询机制:

image-20210716070234915

Nacos 服务端收到请求之后,先检查配置是否发生了变更,如果没有,则设置一个定时任务,延期 29.5s 执行,并且把当前的客户端长轮询连接加入 allSubs 队列。这时候有两种方式触发该连接结果的返回:

• 第一种是在等待 29.5s 后触发自动检查机制,这时候不管配置有没有发生变化,都会把结果返回客户端。而 29.5s 就是这个长连接保持的时间。

• 第二种是在 29.5s 内任意一个时刻,通过 Nacos Dashboard 或者 API 的方式对配置进行了修改,这会触发一个事件机制,监听到该事件的任务会遍历 allSubs 队列,找到发生变更的配置项对应的 ClientLongPolling 任务,将变更的数据通过该任务中的连接进行返回,就完成了一次“推送”操作。

这样既能够保证客户端实时感知配置的变化,也降低了服务端的压力。其中,这个长连接的会话超时时间默认为 30s。

2.微服务高可用-负载均衡Ribbon

介绍

负载均衡将来自客户端的请求按照某种策略平均的分配到集群的每一个节点上,保证这些节点的 CPU、内存等设备负载情况大致在一条水平线,避免由于局部节点负载过高产生宕机,再将这些处理压力传递到其他节点上产生系统性崩溃。

分类:按实现方式可区分为服务端负载均衡与客户端负载均衡。

服务端负载均衡:客户端先发送请求到负载均衡服务器,然后由负载均衡服务器通过负载均衡算法,在众多可用的服务器之中选择一个来处理请求。

服务器端负载均衡又分为两种,一种是硬件负载均衡,还有一种是软件负载均衡。

硬件负载均衡主要通过在服务器节点之前安装专门用于负载均衡的设备,常见的如:F5。

软件负载均衡则主要是在服务器上安装一些具有负载均衡功能的软件来完成请求分发进而实现负载均衡,常见的如:LVS 、 Nginx 、Haproxy。

客户端负载均衡:客户端自己维护一个可用服务器地址列表,在发送请求前先通过负载均衡算法选择一个将用来处理本次请求的服务器,然后再直接将请求发送至该服务器。

Ribbon负载均衡:是一个基于HTTP和TCP的客户端负载均衡器。Ribbon会到注册中心去获取服务端列表,然后进行按照负载均衡策略(如:轮询)访问以到达负载均衡的作用。

image-20210714160800742

Ribbon 执行流程:

  1. 订单服务(order-service)与商品服务(goods-service)实例在启动时向 Nacos 注册;
  2. 订单服务向商品服务发起通信前,Ribbon 向 Nacos 查询商品服务的可用实例列表;
  3. Ribbon 根据设置的负载策略从商品服务可用实例列表中选择实例;
  4. 订单服务实例向商品服务实例发起请求,完成 RESTful 通信;

Ribbon-使用

demo
  1. 提供者服务

    1. 引入依赖

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
      </dependency>
      <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-netflix-ribbon</artifactId>
      <version>${spring-cloud-alibaba.version}</version>
      </dependency>
    2. 配置文件

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      spring:
      application:
      name: customer-service #应用/微服务名字
      cloud:
      nacos:
      discovery:
      server-addr: 192.168.31.102:8848 #nacos服务器地址
      username: nacos #用户名密码
      password: nacos
      server:
      port: 80
    3. 代码

      controller

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10

      import org.springframework.web.bind.annotation.GetMapping;
      import org.springframework.web.bind.annotation.RestController;
      @RestController
      public class ProviderController {
      @GetMapping("/provider/msg")
      public String sendMessage(){
      return "This is the message from provider service!";
      }
      }
  2. 消费者服务

    1. 引入依赖

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
      </dependency>
      <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-netflix-ribbon</artifactId>
      <version>${spring-cloud-alibaba.version}</version>
      </dependency>
    2. 配置文件

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      spring:
      application:
      name: consumer-service #应用/微服务名字
      cloud:
      nacos:
      discovery:
      server-addr: localhost:8848 #nacos服务器地址
      username: nacos #用户名密码
      password: nacos
      logging:
      level:
      root: debug
    3. 代码

      启动类

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      @SpringBootApplication
      public class ConsumerServiceApplication {
      //Java Config声明RestTemplate对象
      //在应用启动时自动执行restTemplate()方法创建RestTemplate对象,其BeanId为restTemplate。
      @Bean
      public RestTemplate restTemplate(){
      return new RestTemplate();
      }
      public static void main(String[] args) {
      SpringApplication.run(ConsumerServiceApplication.class, args);
      }
      }

      controller

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      @RestController
      public class ConsumerController {
      private Logger logger = LoggerFactory.getLogger(ConsumerController.class);
      //注入 Ribbon 负载均衡器对象
      //在引入 starter-netflix-ribbo n后在 SpringBoot 启动时会自动实例化 LoadBalancerClient 对象。
      //在 Controlle 使用 @Resource 注解进行注入即可。
      @Resource
      private LoadBalancerClient loadBalancerClient;
      @Resource
      //将应用启动时创建的 RestTemplate 对象注入 ConsumerController
      private RestTemplate restTemplate;
      @GetMapping("/consumer/msg")
      public String getProviderMessage() {
      //loadBalancerClient.choose()方法会从 Nacos 获取 provider-service 所有可用实例,
      //并按负载均衡策略从中选择一个可用实例,封装为 ServiceInstance(服务实例)对象
      //结合现有环境既是从192.168.31.111:80、192.168.31.112:80、192.168.31.113:80三个实例中选择一个包装为ServiceInstance
      ServiceInstance serviceInstance = loadBalancerClient.choose("provider-service");
      //获取服务实例的 IP 地址
      String host = serviceInstance.getHost();
      //获取服务实例的端口
      int port = serviceInstance.getPort();
      //在日志中打印服务实例信息
      logger.info("本次调用由provider-service的" + host + ":" + port + " 实例节点负责处理" );
      //通过 RestTemplate 对象的 getForObject() 方法向指定 URL 发送请求,并接收响应。
      //getForObject()方法有两个参数:
      //1. 具体发送的 URL,结合当前环境发送地址为:http://192.168.31.111:80/provider/msg
      //2. String.class说明 URL 返回的是纯字符串,如果第二参数是实体类, RestTemplate 会自动进行反序列化,为实体属性赋值
      String result = restTemplate.getForObject("http://" + host + ":" + port + "/provider/msg", String.class);
      //输出响应内容
      logger.info("provider-service 响应数据:" + result);
      //向浏览器返回响应
      return "consumer-service 响应数据:" + result;
      }
      }
  3. 启动1个提供者,3个消费者

    启动消费者,可以启动一个,修改端口后,再启动下一个

  4. 测试

    默认轮询

    访问:http://192.168.31.120/consumer/msg,查看后台日志.

    1
    2
    3
    4
    5
    6
    7
    8
    本次调用由 provider-service 的 192.168.31.111:80 实例节点负责处理
    consumer-service 获得数据:This is the message from provider service!
    本次调用由 provider-service 的 192.168.31.112:80 实例节点负责处理
    consumer-service 获得数据:This is the message from provider service!
    本次调用由 provider-service 的 192.168.31.113:80 实例节点负责处理
    consumer-service 获得数据:This is the message from provider service!
    本次调用由 provider-service 的 192.168.31.111:80 实例节点负责处理
    consumer-service 获得数据:This is the message from provider service!
负载均衡策略配置
  • RoundRobinRule:轮询策略,Ribbon 默认策略。默认超过 10 次获取到的 server 都不可用,会返回⼀个空的 server。
  • RandomRule:随机策略,如果随机到的 server 为 null 或者不可用的话。会不停地循环选取。

  • RetryRule:重试策略,⼀定时限内循环重试。默认继承 RoundRobinRule,也⽀持自定义注⼊,RetryRule 会在每次选取之后,对选举的 server 进⾏判断,是否为 null,是否 alive,并且在 500ms 内会不停地选取判断。而 RoundRobinRule 失效的策略是超过 10 次,RandomRule 没有失效时间的概念,只要 serverList 没都挂。

  • BestAvailableRule:最小连接数策略,遍历 serverList,选取出可⽤的且连接数最小的⼀个 server。那么会调用 RoundRobinRule 重新选取。

  • AvailabilityFilteringRule:可用过滤策略。扩展了轮询策略,会先通过默认的轮询选取⼀个 server,再去判断该 server 是否超时可用、当前连接数是否超限,都成功再返回。

  • ZoneAvoidanceRule:区域权衡策略。扩展了轮询策略,除了过滤超时和链接数过多的 server,还会过滤掉不符合要求的 zone 区域⾥⾯的所有节点,始终保证在⼀个区域/机房内的服务实例进行轮询。

配置文件修改策略
1
2
3
provider-service: #服务提供者的微服务id
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule #设置对应的负载均衡类

3.服务间通信OpenFeign

OpenFeign 是Spring官方在 Netflix Feign 的基础上进行封装的技术,结合原有 Spring MVC 的注解,对 Spring Cloud 微服务通信提供了良好的支持。

OpenFeign 开发的方式与开发 Spring MVC Controller 颇为相似。

OpenFeign使用

feign使用流程:
1.导包
2.yml配置到eureka中
3.启动类增加注解@EnableDiscoveryClient
4.生产者消费者写一样的接口,者接口上写@feignclient
5.生产者有订单接口的具体实现
6.消费者写法同平常的controller

demo
  1. 提供者服务

    1. 引入依赖

      1
      2
      3
      4
      5
      6
      7
      8
      <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
      </dependency>
      <dependency>
      <groupId>com.alibaba.cloud</groupId>
      <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
      </dependency>
    2. 配置文件

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      spring:
      application:
      name: warehouse-service #应用/微服务名字
      cloud:
      nacos:
      discovery:
      server-addr: 192.168.31.102:8848 #nacos服务器地址
      username: nacos #用户名密码
      password: nacos
      server:
      port: 80
    3. 代码

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      @RestController
      public class WarehouseController {
      /**
      * 查询对应 skuId 的库存状况
      * @param skuId skuId
      * @return Stock 库存对象
      */
      @GetMapping("/stock")
      public Stock getStock(Long skuId){
      Map result = new HashMap();
      Stock stock = null;
      if(skuId == 1101l){
      //模拟有库存商品
      stock = new Stock(1101l, "Apple iPhone 11 128GB 紫色", 32, "台");
      stock.setDescription("Apple 11 紫色版对应商品描述");
      }else if(skuId == 1102l){
      //模拟无库存商品
      stock = new Stock(1101l, "Apple iPhone 11 256GB 白色", 0, "台");
      stock.setDescription("Apple 11 白色版对应商品描述");
      }else{
      //演示案例,暂不考虑无对应 skuId 的情况
      }
      return stock;
      }
      }
  2. 消费者服务

    1. 引入依赖

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
      </dependency>
      <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-openfeign</artifactId>
      <version>2.2.5.RELEASE</version>
      </dependency>
    2. 配置文件

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      spring:
      application:
      name: order-service
      cloud:
      nacos:
      discovery:
      server-addr: 192.168.31.102:8848
      username: nacos
      password: nacos
      server:
      port: 80
    3. 代码

      启动类

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      import org.springframework.boot.SpringApplication;
      import org.springframework.boot.autoconfigure.SpringBootApplication;
      import org.springframework.cloud.openfeign.EnableFeignClients;
      @SpringBootApplication
      @EnableFeignClients //启用OpenFeign
      public class OrderServiceApplication {
      public static void main(String[] args) {
      SpringApplication.run(OrderServiceApplication.class, args);
      }
      }

      OpenFeign通信接口

      1
      2
      3
      4
      5
      6
      7
      8
      import org.springframework.cloud.openfeign.FeignClient;
      import org.springframework.web.bind.annotation.GetMapping;
      import org.springframework.web.bind.annotation.RequestParam;
      @FeignClient("warehouse-service") //说明当前接口为 OpenFeign 通信客户端,参数值 warehouse-service 为服务提供者 ID,这一项必须与 Nacos 注册 ID 保持一致。
      public interface WarehouseServiceFeignClient {
      @GetMapping("/stock")
      public Stock getStock(@RequestParam("skuId") Long skuId);
      }

      Controller

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      import org.springframework.web.bind.annotation.GetMapping;
      import org.springframework.web.bind.annotation.RestController;
      import javax.annotation.Resource;
      import java.util.LinkedHashMap;
      import java.util.Map;
      @RestController
      public class OrderController {
      //利用@Resource将IOC容器中自动实例化的实现类对象进行注入
      @Resource
      private WarehouseServiceFeignClient warehouseServiceFeignClient;
      /**
      * 创建订单业务逻辑
      * @param skuId 商品类别编号
      * @param salesQuantity 销售数量
      * @return
      */
      @GetMapping("/create_order")
      public Map createOrder(Long skuId , Long salesQuantity){
      Map result = new LinkedHashMap();
      //查询商品库存,像调用本地方法一样完成业务逻辑。
      Stock stock = warehouseServiceFeignClient.getStock(skuId);
      System.out.println(stock);
      if(salesQuantity <= stock.getQuantity()){
      //创建订单相关代码,此处省略
      //CODE=SUCCESS代表订单创建成功
      result.put("code" , "SUCCESS");
      result.put("skuId", skuId);
      result.put("message", "订单创建成功");
      }else{
      //code=NOT_ENOUGN_STOCK代表库存不足
      result.put("code", "NOT_ENOUGH_STOCK");
      result.put("skuId", skuId);
      result.put("message", "商品库存数量不足");
      }
      return result;
      }
      }
OpenFeign执行流程
  1. 第一次访问 WarehouseServiceFeignClient (OpenFeign)接口时,Spring 自动生成接口的实现类并实例化对象。
  2. 调用 getStock() 方法时,Ribbon 获取 warehouse-service 可用实例信息,根据负载均衡策略选择合适实例。
  3. OpenFeign 根据方法上注解描述的映射关系生成完整的 URL 并发送 HTTP 请求,如果请求方法是 @PostMapping,则参数会附加在请求体中进行发送。
  4. warehouse-service 处理完毕返回 JSON 数据,消费者端 OpenFeign 接收 JSON 的同时反序列化到 Stock 对象,并将该对象返回。
高级配置

负载均衡+数据压缩

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# OpenFeign负载均衡策略,默认引用Ribbon实现客户端负载均衡
warehouse-service: #服务提供者的微服务ID
ribbon:
#设置对应的负载均衡类
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule

# OpenFeign 数据压缩功能
feign:
compression:
request:
# 开启请求数据的压缩功能
enabled: true
# 压缩支持的MIME类型
mime-types: text/xml,application/xml, application/json
# 数据压缩下限 1024表示传输数据大于1024 才会进行数据压缩(最小压缩值标准) 压缩后尺寸只相当于原始数据的 10%~30%,通过CPU计算,极大提高带宽利用率。CPU 负载长期超过 70% 不适合开启。
min-request-size: 1024
# 开启响应数据的压缩功能
response:
enabled: true

替换默认通信组件

基于 Apache HttpClient、OKHttp 组件自带的连接池,可以更好地对 HTTP 连接对象进行重用与管理。

  1. 引入依赖

    1
    2
    3
    4
    5
    <dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-okhttp</artifactId>
    <version>11.0</version>
    </dependency>
  2. 应用入口,利用 Java Config 形式初始化 OkHttpClient 对象。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    @SpringBootApplication
    @EnableFeignClients
    public class OrderServiceApplication {
    //Spring IOC容器初始化时构建okHttpClient对象
    @Bean
    public okhttp3.OkHttpClient okHttpClient(){
    return new okhttp3.OkHttpClient.Builder()
    //读取超时时间
    .readTimeout(10, TimeUnit.SECONDS)
    //连接超时时间
    .connectTimeout(10, TimeUnit.SECONDS)
    //写超时时间
    .writeTimeout(10, TimeUnit.SECONDS)
    //设置连接池
    .connectionPool(new ConnectionPool())
    .build();
    }
    public static void main(String[] args) {
    SpringApplication.run(OrderServiceApplication.class, args);
    }
    }
  3. 配置启用 OKHttp

    1
    2
    3
    feign:
    okhttp:
    enabled: true

同类产品Dubbo

4.微服务大门-网关GateWay

介绍

场景
  • 用户身份鉴权
  • 日志记录
  • 黑白名单
  • 反爬虫
网关作用
  • 解耦
  • 统一的一个入口,方便做统一的事。

image-20210715142325376

Gateway特点
  • 基于 NIO 异步处理,有更好的性能。

  • 配置简单

  • 基于 JDK 8+ 开发

  • 基于 Spring Framework 5 + Project Reactor + Spring Boot 2.0 构建

  • 支持动态路由,能够匹配任何请求属性上的路由;

  • 基于 HTTP 请求的路由匹配(Path、Method、Header、Host 等)

  • 过滤器可以修改 HTTP 请求和 HTTP 响应(增加/修改 Header、增加/修改请求参数、改写请求 Path 等等);

使用

前提

service-a提供了三个RESTFul接口

image-20210715160003080

service-b提供了三个接口

image-20210715160021668

Gatway-demo
  1. 引入依赖

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    <!-- Nacos客户端 -->
    <dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>
    <!-- Spring Cloud Gateway Starter -->
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-gateway</artifactId>
    </dependency>
    <!-- 对外提供Gateway应用监控指标 -->
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
  2. application.yml配置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    spring:
    application:
    name: gateway #配置微服务id
    cloud:
    nacos:
    discovery:
    server-addr: 192.168.31.101:8848 #nacos通信地址
    username: nacos
    password: nacos
    gateway: #让gateway通过nacos实现自动路由转发
    discovery:
    locator:
    enabled: true #locator.enabled 开启自动转发,根据URL规则实现默认路由转发
    server:
    port: 80 #服务端口号
    management:
    endpoints:
    web:
    exposure:
    include: '*' #对外暴露actuator所有监控指标,便于监控系统收集跟踪
  3. 启动服务并访问

    1
    2
    3
    4
    // 规则
    http://网关IP:端口/微服务id/URI

    http://192.168.31.103:80/service-a/list
高级配置

路由、谓词和过滤器。

路由(Route)是指一个完整的网关地址映射与处理过程。(前端的请求经过网关,网关判断请求转发到)一个完整的路由包含两部分配置:谓词(Predicate)与过滤器(Filter)。前端应用发来的请求要被转发到哪个微服务上,是由谓词决定的;而转发过程中请求、响应数据被网关如何加工处理是由过滤器决定的。

谓词

指定时点后路由规则生效。

1
2
predicates:
- After=2020-10-04T00:00:00.000+08:00

URI 符合映射规则时生效

1
2
predicates:
- Path=/b/**

Header 包含指定请求头时生效

1
2
predicates:
- Header=X-Request-Id, \d+
过滤器

对所有匹配的请求添加一个查询参数。

1
2
filters:
- AddRequestParameter=foo,bar #在请求参数中追加foo=bar

返回客户端之前,添加响应数据

1
2
3
4
# 在Response中添加Header头,key=X-Response-Foo,Value=Bar。
# 返回客户端之前,Header添加响应数据
filters:
- AddResponseHeader=X-Response,Blue

返回 503 状态码的响应后,Retry 过滤器重新发起请求,最多重试 3 次。

1
2
3
4
5
6
filters:
#涉及过滤器参数时,采用name-args的完整写法
- name: Retry #name是内置的过滤器名
args: #参数部分使用args说明
retries: 3
status: 503
实例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
spring:
application:
name: gateway
cloud:
nacos:
discovery:
server-addr: 192.168.31.10:8848
username: nacos
password: nacos
gateway:
# 网关跨域
globalcors:
cors-configurations:
'[/**]':
# 允许携带认证信息
# 允许跨域的源(网站域名/ip),设置*为全部
# 允许跨域请求里的head字段,设置*为全部
# 允许跨域的method, 默认为GET和OPTIONS,设置*为全部
# 跨域允许的有效期
allow-credentials: true
allowed-origins: "*"
allowed-headers: "*"
allowed-methods: "*"
max-age: 3600
discovery:
locator:
enabled: false #不再需要Gateway路由转发
routes: #路由规则配置
#第一个路由配置,service-a路由规则
- id: service_a_route #路由唯一标识
#lb开头代表基于gateway的负载均衡策略选择实例
uri: lb://service-a
#谓词配置
predicates:
#Path路径谓词,代表用户端URI如果以/a开头便会转发到service-a实例
- Path=/a/**
#After生效时间谓词,2020年10月15日后该路由才能在网关对外暴露
- After=2020-10-05T00:00:00.000+08:00[Asia/Shanghai]
#谓词配置
filters:
#忽略掉第一层前缀进行转发
- StripPrefix=1
#为响应头附加X-Response=Blue 2020-10-15后此项为默认
- AddResponseHeader=X-Response,Blue
#第二个路由配置,service-b路由规则
- id: service_b_route
uri: lb://service-b
predicates:
- Path=/b/**
filters:
- StripPrefix=1
server:
port: 80
management:
endpoints:
web:
exposure:
include: '*'

跨域

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 全局跨域配置
* 注意:前端从网关进行调用时需要配置
* 跨域配置文件在 src/main/resources/bootstrap.yml
*/
@Configuration
@ConditionalOnBean(GlobalCorsProperties.class)
public class GlobalCorsConfig {
@Autowired
private GlobalCorsProperties globalCorsProperties;
@Bean
public CorsWebFilter corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(new PathPatternParser());
globalCorsProperties.getCorsConfigurations().forEach((path,corsConfiguration)->source.registerCorsConfiguration(path, corsConfiguration));
return new CorsWebFilter(source);
}

}

原理

路由转发流程

image-20210715160939510

内部执行原理

image-20210715165118189

  1. Spring Cloud Gateway 启动时基于 Netty Server 监听指定的端口(该端口可以通过 server.port 属性自定义)。当前端应用发送一个请求到网关时,进入 Gateway Handler Mapping 处理过程,网关会根据当前 Gateway 所配置的谓词(Predicate)来决定是由哪个微服务进行处理。
  2. 确定微服务后,请求向后进入 Gateway Web Handler 处理过程,该过程中 Gateway 根据过滤器(Filters)配置,将请求按前后顺序依次交给 Filter 过滤链进行前置(Pre)处理,前置处理通常是对请求进行前置检查,例如:判断是否包含某个指定请求头、检查请求的 IP 来源是否合法、请求包含的参数是否正确等。
  3. 当过滤链前置(Pre)处理完毕后,请求会被 Gateway 转发到真正的微服务实例进行处理,微服务处理后会返回响应数据,这些响应数据会按原路径返回被 Gateway 配置的过滤链进行后置处理(Post),后置处理通常是对响应进行额外处理,例如:将处理过程写入日志、为响应附加额外的响应头或者流量监控等。

5.服务流量控制Sentinel

背景

微服务环境下受制于网络、机器性能、算法、程序各方面影响,运行异常的情况也在显著提升,加上并发量不可控,需要做好异常保护。

微服务雪崩:在微服务项目中指由于突发流量导致某个服务不可用,从而导致上游服务不可用,并产生级联效应,最终导致整个系统不可用。例子:服务A调⽤服务B,此时⼤量请求突然请求服务A,假如服务A本身能抗住这些请

求,但是如果服务B抗不住,导致服务B请求堆积,从而服务A请求堆积,直到服务A不可用。

如何避免雪崩效应:

  • 限流

    控制请求的流入,让流量有序的进入应用,保证流量在一个可控的范围内。

  • 服务降级

    当应用处理时间超过规定上限后,无论服务是否处理完成,响应返回预先设置的异常信息。

  • 服务熔断

    设置服务为不可用,发送心跳包,服务可用后再给该服务发请求。

Sentinel介绍

Sentinel 以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。

特征:

  • 丰富的应用场景:承接了阿里巴巴近 10 年的双十一大促流量的核心场景,例如秒杀(即突发流量控制在系统容量可以承受的范围)、消息削峰填谷、集群流量控制、实时熔断下游不可用应用等。
  • 完备的实时监控:Sentinel 同时提供实时的监控功能。您可以在控制台中看到接入应用的单台机器秒级数据,500 台以下规模的集群的汇总运行情况。
  • 广泛的开源生态:Sentinel 提供开箱即用的与其他开源框架/库的整合模块,例如与 Spring Cloud、Dubbo、gRPC 整合只需要引入相应的依赖并进行简单的配置即可快速地接入 Sentinel。
  • 完善的 SPI 扩展点:Sentinel 提供简单易用、完善的 SPI 扩展接口。可以通过实现扩展接口来快速地定制逻辑。例如定制规则管理、适配动态数据源等。

image-20210707065815664

Sentinel使用

demo

Sentinel 分为两个部分:Sentinel Dashboard和Sentinel 客户端。

  1. Sentinel Dashboard

    Sentinel Dashboard 是 Sentinel 配套的可视化控制台与监控仪表盘套件,它支持节点发现,以及健康情况管理、监控(单机和集群)、规则管理和推送的功能。Sentinel Dashboard 是基于 Spring Boot 开发的 WEB 应用,打包后可以直接运行。

    Sentinel Dashboard 监听9100端口实现与微服务的通信。

    image-20210707070040503

    部署Sentinel Dashboard

    1. 访问:https://github.com/alibaba/Sentinel/releases,下载Sentinel Dashboard

    2. 启动Dashboard

      1
      java -jar -Dserver.port=9100 sentinel-dashboard-1.8.0.jar
    3. SentinelUI界面

      http://192.168.31.10:9100

      账号&密码 sentinel&sentinel

  2. Sentinel 客户端

    Sentinel 客户端需要集成在 Spring Boot 微服务应用中,用于接收来自 Dashboard 配置的各种规则,并通过 Spring MVC Interceptor 拦截器技术实现应用限流、熔断保护。

    客户端配置

    1. 创建工程,加入依赖

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      <!-- Nacos客户端Starter-->
      <dependency>
      <groupId>com.alibaba.cloud</groupId>
      <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
      </dependency>
      <!-- Sentinel客户端Starter-->
      <dependency>
      <groupId>com.alibaba.cloud</groupId>
      <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
      </dependency>
      <!-- 对外暴露Spring Boot监控指标-->
      <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-actuator</artifactId>
      </dependency>
    2. 配置Nacos和Sentinel

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      spring:
      application:
      name: sentinel-sample #应用名&微服务id
      cloud:
      sentinel: #Sentinel Dashboard通信地址
      transport:
      dashboard: 192.168.31.10:9100
      eager: true #取消控制台懒加载
      nacos: #Nacos通信地址
      server-addr: 192.168.31.10:8848
      username: nacos
      password: nacos
      server:
      port: 80
      management:
      endpoints:
      web: #将所有可用的监控指标项对外暴露
      exposure: #可以访问 /actuator/sentinel进行查看Sentinel监控项
      include: '*'
    3. 验证

      访问Sentinel Dashboard

      image-20210707070653681

    4. 限流demo

      1. Sentinel Core 写一个Controller,启动微服务

      2. 在Sentinel Dashboard配置限流规则

        每秒钟只允许 1QPS 访问,超出的请求直接服务降级返回异常。

        image-20210709064054534

        image-20210709064208173

      3. 验证

        访问http://localhost/test_flow_rule,浏览器反复刷新。

Dashboard限流配置

在 Sentinel Dashboard 中“簇点链路”,找到需要限流的 URI,点击“+流控”进入流控设置。

Sentinel-Dashboard 加载链路使用懒加载模式,如果在簇点链路没有找到对应的 URI,需要先访问下这个功能对应的 URI

流控规则说明:

  • 资源名:要流控的 URI,在 Sentinel 中 URI 被称为“资源”;
  • 针对来源:默认 default 代表所有来源,可以针对某个微服务或者调用者单独设置;
  • 阈值类型:是按每秒访问数量(QPS)还是并发数(线程数)进行流控;
  • 单机阈值:具体限流的数值是多少。

    image-20210709064702009

    高级选项:

    • 流控模式是指采用什么方式进行流量控制。

      • 直接模式

        List 接口 QPS 超过 1个时限流,浏览器会出现“Blocked by Sentinel”。

        image-20210709064945827

      • 关联

        同 List 接口关联的update 接口 QPS 超过 1 时,再次访问List 接口便会响应“Blocked by Sentinel”。

        image-20210709065247054

      • 链路

        /check

        ​ → /list

        /scan

        两条链路都可以访问到/list

        image-20210709065449276

        当访问 check 接口的QPS 超过 1 时,List 接口就会被限流。而另一条链路从 scan 接口到List 接口的链路则不会受到任何影响。

    • 流控效果

      • 快速失败

        指流量当过限流阈值后,直接返回响应并抛出 BlockException

      • Warm Up(预热)

        用于应对瞬时大并发流量冲击。当遇到突发大流量 Warm Up 会缓慢拉升阈值限制,预防系统瞬时崩溃,这期间超出阈值的访问处于队列等待状态,并不会立即抛出 BlockException。

        List 接口平时单机阈值 QPS 处于低水位:默认为 1000/3 (冷加载因子)≈333,当瞬时大流量进来,10 秒钟内将 QPS 阈值逐渐拉升至 1000,为系统留出缓冲时间,预防突发性系统崩溃。

        image-20210709065732758

      • 排队等待

        采用匀速放行的方式对请求进行处理。如下所示,假设现在有100个请求瞬间进入,那么会出现以下几种情况:

        单机 QPS 阈值=4,代表 250 毫秒匀速放行 1 个请求,其他请求队列等待,共需 25 秒处理完毕;
        
        单机 QPS 阈值=200,代表 5 毫秒匀速放行一个请求,其他请求队列等待,共需 0.5 秒处理完毕;
        
        如果某一个请求在队列中处于等待状态超过 2000 毫秒,则直接抛出 BlockException。
        

        匀速队列只支持 QPS 模式,且单机阈值不得大于 1000。

        image-20210709070050680

Dashboard熔断配置
介绍

微服务的熔断是指在某个服务接口在执行过程中频繁出现故障的情况,我们便认为这种状态是“不可接受”的,立即对当前接口实施熔断。在规定的时间内,所有送达该接口的请求都将直接抛出 BlockException,在熔断期过后新的请求进入看接口是否恢复正常,恢复正常则继续运行,仍出现故障则再次熔断一段时间,以此往复直到服务接口恢复。

熔断过程

image-20210716164340305

熔断设置

Sentinel Dashboard可以设置三种不同的熔断模式:慢调用比例、异常比例、异常数

  • 慢调用比例是指当接口在1秒内“慢处理”数量超过一定比例,则触发熔断。

    image-20210716164555605

    image-20210716164729877

  • 异常比例是指 1 秒内按接口调用产生异常的比例(异常调用数/总数量)触发熔断。

    image-20210716164814290

    image-20210716164900964

  • 异常数是指在 1 分钟内异常的数量超过阈值则触发熔断。

    image-20210716164917058

    image-20210716164932771

Sentinel与Nacos配置中心整合
  1. 引入依赖

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    <!-- Nacos 客户端 Starter-->
    <dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>
    <!-- Sentinel 客户端 Starter-->
    <dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
    </dependency>
    <!-- 对外暴露 Spring Boot 监控指标-->
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
  2. 配置文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    spring:
    application:
    name: sentinel-sample #应用名&微服务 id
    cloud:
    sentinel: #Sentinel Dashboard 通信地址
    transport:
    dashboard: 192.168.31.10:9100
    eager: true #取消控制台懒加载
    nacos: #Nacos 通信地址
    server-addr: 192.168.31.10:8848
    username: nacos
    password: nacos
    jackson:
    default-property-inclusion: non_null
    server:
    port: 80
    management:
    endpoints:
    web: #将所有可用的监控指标项对外暴露
    exposure: #可以访问 /actuator/sentinel进行查看Sentinel监控项
    include: '*'
    logging:
    level:
    root: debug #开启 debug 是学习需要,生产改为 info 即可
  3. 业务代码

    controller

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    @RestController
    public class SentinelSampleController {
    //演示用的业务逻辑类
    @Resource
    private SampleService sampleService;
    /**
    * 流控测试接口
    * @return
    */
    @GetMapping("/test_flow_rule")
    public ResponseObject testFlowRule(){
    //code=0 代表服务器处理成功
    return new ResponseObject("0","success!");
    }

    /**
    * 熔断测试接口
    * @return
    */
    @GetMapping("/test_degrade_rule")
    public ResponseObject testDegradeRule(){
    try {
    sampleService.createOrder();
    }catch (IllegalStateException e){
    //当 createOrder 业务处理过程中产生错误时会抛出IllegalStateException
    //IllegalStateException 是 JAVA 内置状态异常,在项目开发时可以更换为自己项目的自定义异常
    //出现错误后将异常封装为响应对象后JSON输出
    return new ResponseObject(e.getClass().getSimpleName(),e.getMessage());
    }
    return new ResponseObject("0","order created!");
    }
    }

    返回结果封装类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    /**
    * 封装响应数据的对象
    */
    public class ResponseObject {
    private String code; //结果编码,0-固定代表处理成功
    private String message;//响应消息
    private Object data;//响应附加数据(可选)

    public ResponseObject(String code, String message) {
    this.code = code;
    this.message = message;
    }
    //Getter/Setter省略
    }

    service

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    /**
    * 演示用的业务逻辑类
    */
    @Service
    public class SampleService {
    //模拟创建订单业务
    public void createOrder(){
    try {
    //模拟处理业务逻辑需要101毫秒
    Thread.sleep(101);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    System.out.println("订单已创建");
    }
    }
  4. 测试

    访问 http://localhost/test_flow_rule

整合-流控规则持久化
  1. 增加依赖

    1
    2
    3
    4
    <dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-datasource-nacos</artifactId>
    </dependency>
  2. 配置文件增加配置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    spring:
    application:
    name: sentinel-sample #应用名&微服务id
    cloud:
    sentinel: #Sentinel Dashboard通信地址
    transport:
    dashboard: 192.168.31.10:9100
    eager: true #取消控制台懒加载
    datasource:
    flow: #数据源名称,可以自定义
    nacos: #nacos配置中心
    #Nacos内置配置中心,因此重用即可
    server-addr: ${spring.cloud.nacos.server-addr}
    dataId: ${spring.application.name}-flow-rules #定义流控规则data-id,完整名称为:sentinel-sample-flow-rules,在配置中心设置时data-id必须对应。
    groupId: SAMPLE_GROUP #gourpId对应配置文件分组,对应配置中心groups项
    rule-type: flow #flow固定写死,说明这个配置是流控规则
    username: nacos #nacos通信的用户名与密码
    password: nacos
    nacos: #Nacos通信地址
    server-addr: 192.168.31.10:8848
    username: nacos
    password: nacos
  3. nacos配置中添加 流控配置

    同UI 页面配置同样的效果

    image-20210717222155344

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    [
    {
    "resource":"/test_flow_rule", #资源名,说明对那个URI进行流控
    "limitApp":"default", #命名空间,默认default
    "grade":1, #类型 0-线程 1-QPS
    "count":2, #超过2个QPS限流将被限流
    "strategy":0, #限流策略: 0-直接 1-关联 2-链路
    "controlBehavior":0, #控制行为: 0-快速失败 1-WarmUp 2-排队等待
    "clusterMode":false #是否集群模式
    }
    ]
  4. 验证流控是否生效

    访问 http://localhost/test_flow_rule

    1
    2
    // 服务启动时已向 Nacos 配置中心获取到流控规则。
    DEBUG 12728 --- [main] s.n.www.protocol.http.HttpURLConnection : sun.net.www.MessageHeader@5432948015 pairs: {GET /nacos/v1/cs/configs?dataId=sentinel-sample-flow-rules&accessToken=eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJuYWNvcyIsImV4cCI6MTYxMDg3NTA1M30.Hq561OkXuAqPI20IBsnPIn0ia86R9sZgdWwa_K8zwvw&group=SAMPLE_GROUP HTTP/1.1: null}...

    在浏览器反复刷新,当 test_flow_rule 接口每秒超过 2 次访问,就会出现“Blocked by Sentinel (flow limiting)”的错误信息,说明流控规则已生效。

  5. 验证能否自动推送新规则

    将Nacos 配置中心中流控规则 count 选项改为 1。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    [
    {
    "resource":"/test_flow_rule",
    "limitApp":"default",
    "grade":1,
    "count":1, #2改为1
    "strategy":0,
    "controlBehavior":0,
    "clusterMode":false
    }
    ]

    新规则发布后,sentinel-sample控制台会立即收到下面的日志,说明新的流控规则即时生效。

    1
    DEBUG 12728 --- [.168.31.10_8848] s.n.www.protocol.http.HttpURLConnection  : sun.net.www.MessageHeader@41257f3915 pairs: {GET /nacos/v1/cs/configs?dataId=sentinel-sample-flow-rules&accessToken=eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJuYWNvcyIsImV4cCI6MTYxMDg3NTA1M30.Hq561OkXuAqPI20IBsnPIn0ia86R9sZgdWwa_K8zwvw&group=SAMPLE_GROUP HTTP/1.1: null}

    通过 Spring Boot Actuator 提供的监控指标确认流控规则已生效。

    访问 http://localhost/actuator/sentinel,在 flowRules 这个数组中,可以看到 test_flow_rule 的限流规则

自定义资源流控

介绍:针对某一个 Service 业务逻辑方法进行限流熔断等规则设置。

实例:对 SampleSerivce.createOrder方法进行熔断保护

  1. 声明切面类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    @SpringBootApplication
    public class SentinelSampleApplication {
    // 注解支持的配置Bean
    @Bean
    public SentinelResourceAspect sentinelResourceAspect() {// 用于进行熔断的前置检查
    return new SentinelResourceAspect();
    }
    public static void main(String[] args) {
    SpringApplication.run(SentinelSampleApplication.class, args);
    }
    }
  2. 声明资源点

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    /**
    * 演示用的业务逻辑类
    */
    @Service
    public class SampleService {
    //资源点名称为createOrder
    @SentinelResource("createOrder")
    /**
    * 模拟创建订单业务
    * 抛出IllegalStateException异常用于模拟业务逻辑执行失败的情况
    */
    public void createOrder() throws IllegalStateException{
    try {
    //模拟处理业务逻辑需要101毫秒
    Thread.sleep(101);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    System.out.println("订单已创建");
    }
    }
  3. 启动服务测试接口,确认资源点已存在 Sentinel Dashboard

    访问 http://localhost/test_degrade_rule

    image-20210717223343479

  4. 获取熔断规则

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    datasource:
    flow: #之前的流控规则,直接忽略
    ...
    degrade: #熔断规则
    nacos:
    server-addr: ${spring.cloud.nacos.server-addr}
    dataId: ${spring.application.name}-degrade-rules
    groupId: SAMPLE_GROUP
    rule-type: degrade #代表熔断
    username: nacos
    password: nacos

    确定配置,启动日志如下

    1
    [main] s.n.www.protocol.http.HttpURLConnection  : sun.net.www.MessageHeader@d96945215 pairs: {GET /nacos/v1/cs/configs?dataId=sentinel-sample-degrade-rules&accessToken=eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJuYWNvcyIsImV4cCI6MTYxMDg5MDMwNH0.ooHkFb4zX14klmHMuLXTDkHSoCrwI8LtN7ex__9tMHg&group=SAMPLE_GROUP HTTP/1.1: null}...
  5. 配置熔断规则

    image-20210717223826845

    1
    2
    3
    4
    5
    6
    7
    8
    9
    [{
    "resource": "createOrder", #自定义资源名
    "limitApp": "default", #命名空间
    "grade": 0, #0-慢调用比例 1-异常比例 2-异常数
    "count": 100, #最大RT 100毫秒执行时间
    "timeWindow": 5, #时间窗口5秒
    "minRequestAmount": 1, #最小请求数
    "slowRatioThreshold": 0.1 #比例阈值
    }]

    熔断机制

    image-20210717224019474

    访问 Spring Boot Actuatorhttp://localhost/actuator/sentinel,可以看到此时 gradeRules 数组下 createOrder 资源点的熔断规则已被 Nacos推送并立即生效。

  6. 验证

    连续访问 http://localhost/test_degrade_rule,当第二次访问时便会出现 500 错误。

    image-20210717224132076

  7. 完善异常提示

    针对 RESTful 接口的统一异常处理需要实现 BlockExceptionHandler

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    @Component //Spring IOC实例化并管理该对象
    public class UrlBlockHandler implements BlockExceptionHandler {
    /**
    * RESTFul异常信息处理器
    * @param httpServletRequest
    * @param httpServletResponse
    * @param e
    * @throws Exception
    */
    @Override
    public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, BlockException e) throws Exception {
    String msg = null;
    if(e instanceof FlowException){//限流异常
    msg = "接口已被限流";
    }else if(e instanceof DegradeException){//熔断异常
    msg = "接口已被熔断,请稍后再试";
    }else if(e instanceof ParamFlowException){ //热点参数限流
    msg = "热点参数限流";
    }else if(e instanceof SystemBlockException){ //系统规则异常
    msg = "系统规则(负载/....不满足要求)";
    }else if(e instanceof AuthorityException){ //授权规则异常
    msg = "授权规则不通过";
    }
    httpServletResponse.setStatus(500);
    httpServletResponse.setCharacterEncoding("UTF-8");
    httpServletResponse.setContentType("application/json;charset=utf-8");
    //ObjectMapper是内置Jackson的序列化工具类,这用于将对象转为JSON字符串
    ObjectMapper mapper = new ObjectMapper();
    //某个对象属性为null时不进行序列化输出
    mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
    mapper.writeValue(httpServletResponse.getWriter(),
    new ResponseObject(e.getClass().getSimpleName(), msg)
    );
    }
    }

    自定义资源点的异常处理

    在 @SentinelResource 注解上额外附加 blockHandler属性进行异常处理

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    /**
    * 演示用的业务逻辑类
    */
    @Service
    public class SampleService {
    @SentinelResource(value = "createOrder",blockHandler = "createOrderBlockHandler")
    /**
    * 模拟创建订单业务
    * 抛出 IllegalStateException 异常用于模拟业务逻辑执行失败的情况
    */
    public void createOrder() throws IllegalStateException{
    try {
    //模拟处理业务逻辑需要 101 毫秒
    Thread.sleep(101);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    System.out.println("订单已创建");
    }
    public void createOrderBlockHandler(BlockException e) throws IllegalStateException{
    String msg = null;
    if(e instanceof FlowException){//限流异常
    msg = "资源已被限流";
    }else if(e instanceof DegradeException){//熔断异常
    msg = "资源已被熔断,请稍后再试";
    }else if(e instanceof ParamFlowException){ //热点参数限流
    msg = "热点参数限流";
    }else if(e instanceof SystemBlockException){ //系统规则异常
    msg = "系统规则(负载/....不满足要求)";
    }else if(e instanceof AuthorityException){ //授权规则异常
    msg = "授权规则不通过";
    }
    throw new IllegalStateException(msg);
    }
    }

    createOrderBlockHandler 方法的书写有两个要求

    方法返回值、访问修饰符、抛出异常要与原始的 createOrder 方法完全相同。

    createOrderBlockHandler 方法名允许自定义,但最后一个参数必须是 BlockException 对象,这是所有规则异常的父类,通过判断 BlockException 我们就知道触发了哪种规则异常。

Sentinel原理

Sentinel Dashboard 是Sentinel的控制端,当内置在微服务内的 Sentinel Core(客户端)接收到控制端新的限流、熔断规则后,微服务便会自动启用的相应的保护措施。

Sentinel执行流程:

  1. Sentinel Core与Sentinel Dashboard建立连接
  2. Sentinel Dashboard 向Sentinel Core下发新的保护规则
  3. Sentinel Core应用新的保护规则,实施限流、熔断等。

Sentinel执行流程细节:

  1. 建立连接

    Sentinel core 初始化时,主动向application.yml中配置的Dashboard的IP地址发起连接请求。

    1
    2
    3
    4
    5
    6
    #Sentinel Dashboard通信地址
    spring:
    cloud:
    sentinel:
    transport:
    dashboard: 192.168.31.10:9100

    该请求以心跳包的方式定时向Dashboard发送,包含Sentinel Core 的AppName、IP、端口信息。

    Sentinel Core持续接收Dashboard数据和发送心跳包使用8719 端口。

    Sentinel Dashboard 接收到心跳包后,来自 Sentinel Core的AppName、IP、端口信息会被封装为 MachineInfo 对象放入 ConcurrentHashMap 保存在 JVM的内存中,以备后续使用。

  2. Dashboard下发保护规则

    1. Dashboard 页面中设置了新的保护规则,
    2. 从当前的 MachineInfo 中提取符合要求的微服务实例信息
    3. 通过 Dashboard内置的 transport 模块将新规则打包推送到微服务实例的 Sentinel Core
  3. Sentinel Core应用新规则

    1. Sentinel Core收 到新规则

    2. 对本地规则进行更新。

    3. 依据规则处理请求。

      Sentinel Core 为服务限流、熔断提供了核心拦截器 SentinelWebInterceptor,这个拦截器默认对所有请求 /** 进行拦截,然后开始请求的链式处理流程,在对于每一个处理请求的节点被称为 Slot(槽),通过多个槽的连接形成处理链,在请求的流转过程中,如果有任何一个 Slot 验证未通过,都会产生 BlockException,请求处理链便会中断,并返回“Blocked by sentinel” 异常信息。

      image-20210708211817988

      默认 Slot 有7 个,前 3 个 Slot为前置处理,用于收集、统计、分析必要的数据;后 4 个为规则校验 Slot,从Dashboard 推送的新规则保存在“规则池”中,然后对应 Slot 进行读取并校验当前请求是否允许放行,允许放行则送入下一个 Slot 直到最终被 RestController 进行业务处理,不允许放行则直接抛出 BlockException 返回响应。

      Slot具体职责:

      • NodeSelectorSlot 负责收集资源的路径,并将这些资源的调用路径,以树状结构存储起来,用于根据调用路径来限流降级;
      • ClusterBuilderSlot 则用于存储资源的统计信息以及调用者信息,例如该资源的 RT(运行时间), QPS, thread count(线程总数)等,这些信息将用作为多维度限流,降级的依据;
      • StatistcSlot 则用于记录,统计不同维度的runtime 信息;
      • SystemSlot 则通过系统的状态,例如CPU、内存的情况,来控制总的入口流量;
      • AuthoritySlot 则根据黑白名单,来做黑白名单控制;
      • FlowSlot 则用于根据预设的限流规则,以及前面 slot 统计的状态,来进行限流;
      • DegradeSlot 则通过统计信息,以及预设的规则,来做熔断降级。

6.应用性能监控SkyWalking

博客地址 TODO

Sleuth+Zipkin

使用 Tracer 在访问链路中创建自定义的 Span

介绍:创建自定义的 Span 并纳入可视化监控机制中

场景:在业务系统中重点监控某些业务操作

版本1.X

实现:通过 Spring Cloud Sleuth 自带的 org.springframework.cloud.sleuth.Tracer 接口创建和管理自定义 Span。

版本:2.X

实现:使用 Brave 创建自定义 Span

代码:

  1. 业务中添加代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    @Service
    public class MyService {

    @Autowired
    private Tracer tracer;//

    public void perform() {
    // 创建并启动了一个“spanName”新的 Span
    Span newSpan = tracer.nextSpan().name("spanName").start();
    //ScopedSpan newSpan = tracer.startScopedSpan("spanName");
    try {
    //执行业务逻辑
    }
    finally{
    newSpan.tag("key", "value");
    newSpan.annotate("myannotation");
    newSpan.finish();
    }
    }
    }
  2. 注解

    1
    2
    @NewSpan(name = "myspan")//创建新span
    void myMethod(@SpanTag("mykey") String param);//生成一个键为“mykey”,值为 param 的新标签。

7.分布式事务Seata

介绍

产生原因
  1. 跨数据库操作

  2. 跨系统的分布式事务

    与第三方系统(企业内外)集成可能遇到

  3. 跨服务的分布式事务

  4. 跨数据库与消息的分布式事务

分布式解决方案

分布式事务解决方案:二阶段提交(2PC)与三阶段提交(3PC)。

三阶段提交

image-20210716171516747

  1. 阶段一,事务预处理阶段。

    事务协调者会向各服务下达“处理本地事务”的通知,本地事务开始处理业务逻辑,如订单服务中负责创建新的订单记录;会员服务负责增加会员的积分;库存服务负责减少库存数量。被操作的所有数据都处于未提交(uncommit)的状态,会被排它锁锁定。本地事务都处理完成后,会通知事务协调者“本地事务处理完毕”。所有本地事务处理完毕后,进入阶段二。

  2. 阶段二,预提交阶段。

    预提交阶段只是一个询问机制,以确认所有服务都已准备好,同时在此阶段协调者和参与者都设置了超时时间以防止出现长时间资源锁定。当阶段二所有服务返回“可以提交”,进入阶段三“提交阶段”。

  3. 阶段三,提交阶段

    3PC 的提交阶段与 2PC 的提交阶段是一致的,在每一个数据库中执行提交实现数据的资源写入,如果协调者与服务通信中断导致无法提交,在服务端超时后在也会自动执行提交操作来保证资源释放。

问题:预提交阶段引入了超时机制,让数据库资源不会被长期锁定,数据一致性也很可能因为超时后的强制提交被破坏。

解决:增加异步的数据补偿任务、日终跑批前的数据补偿、更完善的业务数据完整性的校验代码、引入数据监控及时通知人工补录。

Seata

AT:需要数据库支持事务,使用简单。(自动解析sql生成反向SQL,用于回滚。)

TCC:开启、提交、回滚程序员控制,灵活,复杂(失败时,事务自动多次提交,还需考虑幂等性)

使用

AT 模式

image-20210717093558206

部署TC-Seata-Server
  1. 下载seata

    https://github.com/seata/seata/releases/download/v1.4.0/seata-server-1.4.0.tar.gz

  2. 配置seata接入注册中心和配置中心, conf/registry.conf 文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    registry {
    # Seata-Server支持以下几种注册中心,这里改为nacos,默认是file文件形式不介入任何注册中心。
    # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
    type = "nacos"
    # 负载均衡采用随机策略
    loadBalance = "RandomLoadBalance"
    loadBalanceVirtualNodes = 10
    # nacos注册中心接入配置
    nacos {
    # 应用名称
    application = "seata-server"
    #IP地址与端口
    serverAddr = "192.168.31.10:8848"
    # 分配应用组,采用默认值SEATA_GROUP即可
    group = "SEATA_GROUP"
    namespace = ""
    # 集群名称,采用默认值default即可
    cluster = "default"
    # Nacos接入用户名密码
    username = "nacos"
    password = "nacos"
    }
    }
    #Seata-Server接入配置中心
    config {
    # Seata-Server支持以下配置中心产品,这里设置为nacos,默认是file即文件形式保存配置内容。
    # file、nacos 、apollo、zk、consul、etcd3
    type = "nacos"
    # 设置Nacos的通信地址
    nacos {
    serverAddr = "192.168.31.10:8848"
    namespace = ""
    group = "SEATA_GROUP"
    username = "nacos"
    password = "nacos"
    }
    }
  3. nacos配置中心初始化seata配置

    官网找初始化脚本https://github.com/seata/seata/blob/1.4.0/script/config-center/config.txt

    image-20210717093755192

    在 /usr/local/seata-server-1.4.0 目录创建 config.txt文件,复制脚本内容到文件中

    1
    2
    3
    4
    # 修改
    store.db.url=jdbc:mysql://192.168.31.103:3309/seata?useUnicode=true&rewriteBatchedStatements=true
    store.db.user=root
    store.db.password=root

    下载运行脚本

    https://github.com/seata/seata/blob/1.4.0/script/config-center/nacos/nacos-config.sh

    在 /usr/local/seata-server-1.4.0 目录创建 script 子目录。放入文件

    运行导入脚本

    1
    sh nacos-config.sh -h 192.168.31.10

    Nacos 后台http://192.168.31.10:8848/nacos ,会看到大量 SEATA_GROUP 分组的配置,Seata-Server 启动时自动读取

  4. 创建并初始化Seata-Server全局事务数据库

    下载 SQL 脚本 https://github.com/seata/seata/blob/1.4.0/script/server/db/mysql.sql

    MySQL中创建数据库 seata,执行SQL脚本创建全局事务表

    image-20210717094319951

  5. 启动 seata-server

    1
    sh bin/seata-server.sh

    启动过程中提示数据库无法访问,说明 IP、端口配置有问题,可以通过 Nacos 配置中心设置 store.db.url 选项,而不是重新导入 config.txt。

    image-20210717094506894

开发RM资源管理器

订单服务、会员服务、库存服务

订单服务
  1. 创建seata-order数据库和 undo_log 表。Seata 强制要求在每个 RM 端数据库创建的表,用于存储反向 SQL 的元数据

    https://github.com/seata/seata/blob/1.4.0/script/client/at/db/mysql.sql

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    # 订单业务表 order
    SET NAMES utf8mb4;
    SET FOREIGN_KEY_CHECKS = 0;
    -- ----------------------------
    -- Table structure for order
    -- ----------------------------
    DROP TABLE IF EXISTS `order`;
    CREATE TABLE `order` (
    `order_id` int(255) NOT NULL AUTO_INCREMENT COMMENT '订单编号',
    `goods_id` int(32) NOT NULL COMMENT '商品编号',
    `member_id` int(32) NOT NULL COMMENT '会员编号',
    `quantity` int(255) NOT NULL COMMENT '购买数量',
    `points` int(255) NOT NULL COMMENT '增加会员积分',
    PRIMARY KEY (`order_id`) USING BTREE
    ) ENGINE = InnoDB AUTO_INCREMENT = 51 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
    -- ----------------------------
    -- Table structure for undo_log
    -- ----------------------------
    DROP TABLE IF EXISTS `undo_log`;
    CREATE TABLE `undo_log` (
    `id` bigint(20) NOT NULL AUTO_INCREMENT,
    `branch_id` bigint(20) NOT NULL,
    `xid` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
    `context` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
    `rollback_info` longblob NOT NULL,
    `log_status` int(11) NOT NULL,
    `log_created` datetime(0) NOT NULL,
    `log_modified` datetime(0) NOT NULL,
    `ext` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
    PRIMARY KEY (`id`) USING BTREE,
    UNIQUE INDEX `ux_undo_log`(`xid`, `branch_id`) USING BTREE
    ) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
    SET FOREIGN_KEY_CHECKS = 1;
  2. 引入依赖

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    <!--Spring Boot JPA-->
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    <!--Web MVC-->
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!--Nacos客户端-->
    <dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>
    <!--seata-->
    <dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
    <exclusions>
    <exclusion>
    <groupId>io.seata</groupId>
    <artifactId>seata-all</artifactId>
    </exclusion>
    <exclusion>
    <groupId>io.seata</groupId>
    <artifactId>seata-spring-boot-starter</artifactId>
    </exclusion>
    </exclusions>
    </dependency>
    <!--seata 客户端最新版-->
    <dependency>
    <groupId>io.seata</groupId>
    <artifactId>seata-all</artifactId>
    <version>1.4.0</version>
    </dependency>
    <!--seata与spring boot starter-->
    <dependency>
    <groupId>io.seata</groupId>
    <artifactId>seata-spring-boot-starter</artifactId>
    <version>1.4.0</version>
    </dependency>
    <!--JDBC驱动-->
    <dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <scope>runtime</scope>
    </dependency>
  3. 配置文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    #seata配置
    seata:
    # 开启seata分布式事务
    enabled: true
    # 事务服务分组名,与naocs一致
    tx-service-group: my_test_tx_group
    # 是否启用数据源代理
    enable-auto-data-source-proxy: true
    # 事务服务配置
    service:
    vgroup-mapping:
    # 事务分组对应集群名称
    my_test_tx_group: default
    grouplist:
    # Seata-Server服务的IP地址与端口
    default: 192.168.31.107:8091
    enable-degrade: false
    disable-global-transaction: false
    # Nacos配置中心信息
    config:
    type: nacos
    nacos:
    namespace:
    serverAddr: 192.168.31.10:8848
    group: SEATA_GROUP
    username: nacos
    password: nacos
    cluster: default
    # Nacos注册中心信息
    registry:
    type: nacos
    nacos:
    application: seata-server
    server-addr: 192.168.31.10:8848
    group : SEATA_GROUP
    namespace:
    username: nacos
    password: nacos
    cluster: default
    # 应用配置
    spring:
    application:
    name: rm-order
    datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://192.168.31.103:3306/seata-order
    username: root
    password: root
    cloud:
    nacos:
    discovery:
    username: nacos
    password: nacos
    server-addr: 192.168.31.10:8848
    jpa:
    show-sql: true
    server:
    port: 8002
    logging:
    level:
    io:
    seata: debug
  4. 开发crud

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    //JPA实体类
    @Entity
    @Table(name = "`order`") //对应order表
    public class Order {
    @Id
    @Column(name = "order_id")
    private Integer id; //订单编号
    private Integer memberId; //会员编号
    @Column(name = "goods_id")
    private Integer goodsId; //商品编号
    private Integer points; //新增积分
    private Integer quantity; //销售数量
    public Order() {
    }
    public Order(Integer id, Integer memberId, Integer goodsId, Integer points, Integer quantity) {
    this.id = id;
    this.memberId = memberId;
    this.points = points;
    this.goodsId = goodsId;
    this.quantity = quantity;
    }
    //...getter & setter
    }
    1
    2
    public interface OrderRepository extends JpaRepository<Order,Integer> {
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    @Service
    public class OrderService {
    @Resource
    private OrderRepository orderRepository;
    @Transactional
    public Order createOrder(Integer orderId,Integer memberId,Integer goodsId,Integer points,Integer quantity){
    return orderRepository.save(new Order(orderId, memberId,goodsId,points,quantity));
    }
    }

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    @RestController
    public class OrderController {
    @Resource
    private OrderService orderService;
    @GetMapping("/create_order")
    public String createOrder(Integer orderId,Integer memberId,Integer goodsId,Integer points,Integer quantity) throws JsonProcessingException {
    Map result = new HashMap<>();
    Order order = orderService.createOrder(orderId,memberId,goodsId,points,quantity);
    result.put("code", "0");
    result.put("message", "create order success");
    return new ObjectMapper().writeValueAsString(result);
    }
    }
  5. 配置 DataSourceProxy 数据源代理类,用于seata自动生成 undo_log 回滚数据,以及自动完成 RM 端分布式事务的提交或回滚操作。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    import com.alibaba.druid.pool.DruidDataSource;
    import io.seata.rm.datasource.DataSourceProxy;
    import org.springframework.boot.context.properties.ConfigurationProperties;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.context.annotation.Primary;
    @Configuration
    public class DataSourceProxyConfig {
    //创建Druid数据源
    @Bean
    @ConfigurationProperties(prefix = "spring.datasource")
    public DruidDataSource druidDataSource() {
    return new DruidDataSource();
    }
    //建立DataSource数据源代理
    @Primary
    @Bean
    public DataSourceProxy dataSource(DruidDataSource druidDataSource) {
    return new DataSourceProxy(druidDataSource);
    }
    }
  6. 启动 rm-order,访问 create_order 接口

    http://192.168.31.106:8002/create_order?orderId=6&memberId=1&goodsId=2&points=20&quantity=200

rm-points 积分服务
rm-storage 库存服务
开发TM 事务管理器
  1. 创建 seata-mall 数据库和undo_log 表

  2. 引入依赖

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>
    <dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
    <exclusions>
    <exclusion>
    <groupId>io.seata</groupId>
    <artifactId>seata-all</artifactId>
    </exclusion>
    <exclusion>
    <groupId>io.seata</groupId>
    <artifactId>seata-spring-boot-starter</artifactId>
    </exclusion>
    </exclusions>
    </dependency>
    <dependency>
    <groupId>io.seata</groupId>
    <artifactId>seata-all</artifactId>
    <version>1.4.0</version>
    </dependency>
    <dependency>
    <groupId>io.seata</groupId>
    <artifactId>seata-spring-boot-starter</artifactId>
    <version>1.4.0</version>
    </dependency>
    <dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <scope>runtime</scope>
    </dependency>
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
    <version>${spring-cloud-alibaba.version}</version>
    </dependency>
  3. 配置文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    #seata配置与rm-order完全相同,省略
    seata:
    ...
    spring:
    application:
    name: tm-mall
    datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://192.168.31.103:3305/seata-mall
    username: root
    password: root
    ...
    server:
    port: 8001
    ...
  4. 启动类增加远程调用注解

    1
    2
    3
    4
    5
    6
    7
    @SpringBootApplication
    @EnableFeignClients
    public class TmMallApplication {
    public static void main(String[] args) {
    SpringApplication.run(TmMallApplication.class, args);
    }
    }
  5. 开发三个远程调用接口

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    //订单服务客户端
    @FeignClient("rm-order")
    public interface OrderFeignClient {
    @GetMapping("/create_order")
    public String createOrder(@RequestParam("orderId") Integer orderId,
    @RequestParam("memberId") Integer memberId,
    @RequestParam("goodsId") Integer goodsId,
    @RequestParam("points") Integer points,
    @RequestParam("quantity") Integer quantity
    );
    }

    //积分服务客户端
    @FeignClient("rm-points")
    public interface PointsFeignClient {
    @GetMapping("/add_points")
    public String addPoints(@RequestParam("memberId") Integer memberId, @RequestParam("points") Integer points);
    }

    //库存服务客户端
    @FeignClient("rm-storage")
    public interface StorageFeignClient {
    @GetMapping("/reduce_storage")
    public String reduceStorage(@RequestParam("goodsId") Integer goodsId, @RequestParam("quantity") Integer quantity);
    }
  6. 开发 MallService,定义全局事务范围。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    @Service
    public class MallService {
    @Resource
    OrderFeignClient orderFeignClient;
    @Resource
    PointsFeignClient pointsFeignClient;
    @Resource
    StorageFeignClient storageFeignClient;

    @GlobalTransactional(name = "seata-group-tx-mall", rollbackFor = {Exception.class}) // 全局事务注解,MallService.sale 方法时通知 TC 开启全局事务,sale 方法执行成功自动通知 TC 进行全局提交;sale 方法抛出异常时自动通知 TC 进行全局回滚。
    public String sale(Integer orderId,Integer memberId,Integer goodsId,Integer points,Integer quantity) {
    String orderResult = orderFeignClient.createOrder(orderId,memberId,goodsId,points,quantity);
    String pointsResult = pointsFeignClient.addPoints(memberId, points);
    String storageResult = storageFeignClient.reduceStorage(goodsId, quantity);
    return orderResult + " / " + pointsResult + " / " + storageResult;
    }
    }
  7. 开发 MallController 对外暴露 sale 接口提供调用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    @RestController
    public class MallController {
    @Resource
    private MallService mallService;
    @GetMapping("/sale")
    public String sale(Integer orderId,Integer memberId,Integer goodsId,Integer points,Integer quantity){
    return mallService.sale(orderId,memberId,goodsId,points,quantity);
    }
    }
  8. 配置 DataSourceProxyConfig,这是所有 TM 与 RM 都要设置的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    @Configuration
    public class DataSourceProxyConfig {
    @Bean
    @ConfigurationProperties(prefix = "spring.datasource")
    public DruidDataSource druidDataSource() {
    return new DruidDataSource();
    }
    @Primary
    @Bean
    public DataSourceProxy dataSource(DruidDataSource druidDataSource) {
    return new DataSourceProxy(druidDataSource);
    }
    }
验证

启动 Nacos、TC、TM、3 个 RM ,

  • 正常验证

    访问 tm-mall 的 sale 接口。

    http://localhost:8001/sale?orderId=6&memberId=1&goodsId=2&points=20&quantity=20

    1
    2
    3
    4
    5
    6
    ## TM端日志
    # 启动全局事务
    i.seata.tm.api.DefaultGlobalTransaction : Begin new global transaction [192.168.31.107:8091:100622589646344192]
    ...
    # 全局事务已提交
    i.seata.tm.api.DefaultGlobalTransaction : [192.168.31.107:8091:100622589646344192] commit status: Committed
    1
    2
    3
    4
    5
    ## RM日志
    # 分支事务已提交
    i.s.c.r.p.c.RmBranchCommitProcessor : branch commit result:xid=192.168.31.107:8091:100622589646344192,branchId=100622590170632192,branchStatus=PhaseTwo_Committed,result code =Success,getMsg =null
    # 清空undo_log表
    i.s.r.d.undo.mysql.MySQLUndoLogManager : batch delete undo log size 1
  • 异常验证

    quantity 设置为 200,超出库存报错,看能否全局回滚。
    http://localhost:8001/sale?orderId=6&memberId=1&goodsId=2&points=20&quantity=200

    1
    2
    3
    4
    5
    6
    // 程序报错
    java.lang.IllegalStateException: 商品库存不足。

    // TM 向 TC 发起全局回滚通知。
    i.seata.tm.api.DefaultGlobalTransaction : Begin new global transaction [192.168.31.107:8091:100626590567763968]
    i.s.c.rpc.netty.AbstractNettyRemoting : io.seata.core.rpc.netty.TmNettyRemotingClient@2e81af7d msgId:1726, body:globalStatus=Rollbacked,ResultCode=Success,Msg=null

    TC 向 RM 下达分支事务回滚通知,RM 收到通知做两件事:第一,根据 undo_log 表生成的反向 SQL,将之前写入的数据撤销;第二,删除 undo_log 数据。

    1
    2
    3
    i.s.r.d.undo.AbstractUndoLogManager      : Flushing UNDO LOG: {"@class":"io.seata.rm.datasource.undo.BranchUndoLog","xid":"192.168.31.107:8091:100626590567763968","branchId":100626590894919681...
    io.seata.rm.AbstractRMHandler : Branch Rollbacking: 192.168.31.107:8091:100626590567763968 100626590894919681 jdbc:mysql://192.168.31.103:3306/seata-order
    i.s.r.d.undo.AbstractUndoLogManager : xid 192.168.31.107:8091:100626590567763968 branch 100626590894919681, undo_log deleted with GlobalFinished

原理

失效

8.分布式锁

用途:控制分布式系统中的不同主机之间对共享资源的访问。

  • 同一应用程序多实例下控制只有一个实例执行。
  • 不同应用程序只有一个程序修改数据。

应用场景

  • 定时拉取上游系统数据,部署了双节点。不用分布式锁,定时任务会执行两次。
  • 防止同一用户对同一接口短时间多次点击。
  • 秒杀系统的库存管理,避免超卖。

实现方案

  • zookeeper

    介绍:利⽤的是zookeeper的临时节点、顺序节点、watch机制来实现的,zookeeper分布式 锁的特点是⾼⼀致性,因为zookeeper保证的是CP,所以由它实现的分布式锁更可靠,不会出现混乱。先同步到一半以上从节点,再返回加锁成功;

    流程:客户端要获取锁,1.在请求下创建临时顺序子节点,2.若节点最小,则获得了锁,执行业务3.使用完锁,则删除该节点,释放锁。

    详细流程:

    1. 客户端在lock节点下创建临时顺序节点。
    2. 2.获取lock下面的所有子节点,如果发现自己创建的子节点序号最小,那么就认为该客户端获取到了锁,执行业务代码。使用完锁后,将该节点删除。
    3. 3.如果自己创建的节点并非lock所有子节点中最小的,找到比自己小的那个节点,同时对其注册事件监听器,监听删除事件。
    4. 4.如果发现比自己小的那个节点被删除,则客户端的Watcher会收到相应通知,此时再次判断自己创建的节点是否是lock子节点中序号最小的,如果是则获取到了锁,如果不是则重复以上步骤(监听比自己小一个节点的删除事件,判断自己是否是最小节点)。
  • redis

    介绍:利⽤redis的setnx、lua脚本、消费订阅等机制来实现的,redis分布式锁的特点是⾼可⽤, 因为redis保证的是AP,所以由它实现的分布式锁可能不可靠,不稳定(⼀旦redis中的数据出现了 不⼀致),可能会出现多个客户端同时加到锁的情况 。

    框架redisson

    流程:

    1. 尝试获取锁,执行加锁lua脚本。用uuid+自己主线程ID加锁,设置锁过期时间默认30秒
    2. 若第一步未获取到锁,则去订阅解锁消息,当获取到锁剩余过期时间后,调用信号量方法堵塞住方法,知道被唤醒或等待超时。
    3. 一旦持有锁的线程释放了锁,就会广播解锁消息。第二步堵塞的线程就会被唤醒并重新尝试获取锁。
    4. 为了解决锁到期业务可能未执行完的情况,设置定时任务看门狗每隔10s给未完成的业务方法的锁续期30秒。
    5. 解锁时,判断是不是自己的锁,不是自己的锁不删除。

代码:

1
2
3
4
5
redisson.lock();
try{
} finally{
redisson.unlock();
}

使用案例

存在的问题:主从节点不同步,锁丢失。从节点也获取到锁。
服务器4个节点不如3个节点。
持久化时每隔1秒刷新,机器宕机恢复时锁丢失。
解决:
红锁:
三个相等得Redis。
超过半数Redis节点加锁成功才算加速成功。
红锁得坑:
不能配从节点

分布式锁的选择:
Redis锁有时就是会丢失,很严格可以选择性能略低的zookeeper.
参考concurrentHashMap的分段锁

9.用户认证与授权

介绍

认证:也就是说对于每一次访问请求,系统都能判断出访问者是否具有合法的身份标识。(是谁)

AT性)。)而每个资源都有一个拥有者(Resource Owner)。这些资源拥有者所拥有的资源统一存放在资源服务器(Resource Server)中。同时,协议规定需要有一台授权服务器(Authorization Server),即专门用来处理对访问请求进行授权的服务器。

对哪些调用关系进行认证与授权:客户端到服务、从服务到服务。

授权协议OAuth2

OAuth2 协议中把需要访问的接口或服务统称为资源,而每个资源都有一个拥有者(Resource Owner)。这些资源拥有者所拥有的资源统一存放在资源服务器(Resource Server)中。同时,协议规定需要有一台授权服务器(Authorization Server),即专门用来处理对访问请求进行授权的服务器。

OAuth2 协议在客户端程序和资源服务器之间设置了一个授权层,所以客户端程序不能直接访问资源服务器,而是只能先登录授权层。资源拥有者会首先授权给客户端,客户端获得授权之后,向授权服务器申请一个 Token,Token 中就包含了权限范围和有效期。然后,客户端使用这个申请到的 Token 向资源服务器申请获取资源,资源服务器就根据 Token 的权限范围和有效期向客户端开放拥有者的资源。

对应到微服务系统中,服务提供者所充当的角色就是资源服务器,而服务消费者就是客户端。

OAuth 2.0 定义了四种授权方式,即密码模式、授权码模式、简化模式和客户端模式。

JWT

介绍 博客地址TODO

JJWT使用demo
  1. 引入依赖

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    <dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-api</artifactId>
    <version>0.11.2</version>
    </dependency>
    <dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-impl</artifactId>
    <version>0.11.2</version>
    <scope>runtime</scope>
    </dependency>
    <dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-jackson</artifactId> <!-- or jjwt-gson if Gson is preferred -->
    <version>0.11.2</version>
    <scope>runtime</scope>
    </dependency>
  2. 创建token

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    @SpringBootTest
    public class JwtTestor {
    /**
    * 创建Token
    */
    @Test
    public void createJwt(){
    //私钥字符串
    String key = "1234567890_1234567890_1234567890";
    //1.对秘钥做BASE64编码
    String base64 = new BASE64Encoder().encode(key.getBytes());
    //2.生成秘钥对象,会根据base64长度自动选择相应的 HMAC 算法
    SecretKey secretKey = Keys.hmacShaKeyFor(base64.getBytes());
    //3.利用JJWT生成Token
    String data = "{\"userId\":123}"; //载荷数据
    String jwt = Jwts.builder().setSubject(data).signWith(secretKey).compact();
    System.out.println(jwt);
    }
    }
  3. 验证token

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    /**
    * 校验及提取JWT数据
    */
    @Test
    public void checkJwt(){
    String jwt = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ7XCJ1c2VySWRcIjoxMjN9In0.1p_VTN46sukRJTYFxUg93CmfR3nJZRBm99ZK0e3d9Hw";
    //私钥
    String key = "1234567890_1234567890_1234567890";
    //1.对秘钥做BASE64编码
    String base64 = new BASE64Encoder().encode(key.getBytes());
    //2.生成秘钥对象,会根据base64长度自动选择相应的 HMAC 算法
    SecretKey secretKey = Keys.hmacShaKeyFor(base64.getBytes());
    //3.验证Token
    try {
    //生成JWT解析器
    JwtParser parser = Jwts.parserBuilder().setSigningKey(secretKey).build();
    //解析JWT
    Jws<Claims> claimsJws = parser.parseClaimsJws(jwt);
    //得到载荷中的用户数据
    String subject = claimsJws.getBody().getSubject();
    System.out.println(subject);
    }catch (JwtException e){
    //所有关于Jwt校验的异常都继承自JwtException
    System.out.println("Jwt校验失败");
    e.printStackTrace();
    }
    }
Spring Cloud Security

实现 OAuth2 协议以及整合 JWT 认证

基于网关的统一用户认证

image-20210713220029696

执行流程
  1. 认证中心微服务负责用户认证任务,在启动时从 Nacos 配置中心抽取 JWT 加密用私钥;

  2. 用户在登录页输入用户名密码,客户端向认证中心服务发起认证请求

    1
    2
    // 请求示例
    http://usercenter/login #认证中心用户认证(登录)地址
  3. 认证中心服务根据输入在用户数据库中进行认证校验,如果校验成功则返回认证中心将生成用户的JSON数据并创建对应的 JWT 返回给客户端

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // 响应数据示例
    {
    "code": "0",
    "message": "success",
    "data": {
    "user": {
    "userId": 1,
    "username": "zhangsan",
    },
    "token": "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ7XCJ1c2VySWRcIjoxLFwidXNlcm5hbWVcIjpcInpoYW5nc2FuXCIsXCJuYW1lXCI6XCLlvKDkuIlcIixcImdyYWRlXCI6XCJub3JtYWxcIn0ifQ.1HtfszarTxLrqPktDkzArTEc4ah5VO7QaOOJqmSeXEM"
    }
    }

  4. 在收到上述 JSON 数据后,客户端将其中 token 数据保存在 localstorage

  5. 客户端向具体某个微服务发起新的请求,这个 JWT 都会附加在请求头或者 cookie 中发往 API 网关,网关将 JWT 再次转发给用户认证服务,此时用户认证服务对 JWT 进行验签,验签成功,查询用户认证与授权的详细数据。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    {
    "code": "0",
    "message": "success",
    "data": {
    "user": { #用户详细数据
    "userId": 1,
    "username": "zhangsan",
    "name": "张三",
    "grade": "normal"
    "age": 18,
    "idno" : 130.......,
    ...
    },
    "authorization":{ #权限数据
    "role" : "admin",
    "permissions" : [{"addUser","delUser","..."}]
    }
    }
    }

  6. 网关根据路由规则将请求与jwt数据和授权数据转发至具体的微服务。

  7. 具体的微服务收到上述 JSON 后,对当前执行的操作进行判断,检查是否拥有执行权限,权限检查通过执行业务代码,权限检查失败返回错误响应。

JWT续签

JWT存在的问题:JWT 生成后失效期是固定的, JWT 的设计本身就不允许生成完全相同的字符串。单靠 JWT 自身特性是无法做到续签。

需解决:很多业务中需要客户端在不改变 JWT 的前提下,实现 JWT 的“续签”功能。

业务问题:10分钟不操作,token失效,跳转登录页面。

解决方案:

  1. 生成的 JWT 设为“永久生效”。
  2. 生成后存到redis集合A中,设置Expire 有效期5分钟。过期后转移到另一个集合B中,有效期5分钟。
  3. 前端发起请求不同时间段的处理。
    • 前端1-5分钟的请求。在redis的集合A中找到携带的token,处理业务。
    • 前端6-10分钟的请求,
      • 在redis的集合A中未找到携带的token,在集合B中找到了,将集合B中的token移到集合A中。处理业务。
    • 前端超过10分钟的请求。在redis的集合A中未找到携带的token,在集合B中也没找到。返回到登录页面

微服务缓存设计

软件缓存分类:

  • Web应用客户端缓存-浏览器
  • 应用层静态资源缓存-前端-CDN、Nginx
  • 服务层多级缓存-后端-内存(分布式Redis,进程内EhCache)

image-20210713161053321

客户端缓存

浏览器层面我们主要是对 HTML 中的图片、CSS、JS、字体这些静态资源进行缓存。

示例:

image-20210713161225780

通过HTTP 的 Expires 响应头控制静态图片的有效期。百度 Logo 的过期时间为 2031 年 2 月 8 日 9 时 26 分 31 秒。在这个时间段内,浏览器会将图片以文件形式缓存在本地,再次访问时会看到“from disk cache”的提示,此时浏览器不再产生与服务器的实际请求,会从本地直接读取缓存图片。

应用层缓存

浏览器负责读取Expires响应头, Expires 在应用层设置,也就是 CDN 与 Nginx 中进行设置。

CDN

介绍

CDN 全称是 Content Delivery Network,即内容分发网络,是互联网静态资源分发的主要技术手段。

场景:大量的上海用户同时要访问千里之外的北京服务器的资源,这么长的通信必然带来高延迟与更多不可控因素影响数据传输。

应用:广域的互联网应用,CDN 几乎是必需的基础设施,它有效解决了带宽集中占用以及数据分发的问题。像 Web 页面中的图片、音视频、CSS、JS 这些静态资源,都可以通过 CDN 服务器就近获取。

CDN执行流程

image-20210713161811213

在互联网应用中,因为 CDN 涉及多地域多节点组网,前期投入成本较高,更多的中小型软件公司通常会选择阿里云、腾讯云等大厂提供的 CDN 服务,通过按需付费的方式降低硬件成本。

阿里云、腾讯云 CDN 除了缓存文件之外,还提供了管理后台能为响应赋予额外的响应头。如下所示在阿里云 CDN 后台,就额外设置了 Cache-Control 响应头代表缓存有效期为 1 小时。这里我们额外提一下 Expires 与的 Cache-Control 的区别,Expires 是指定具体某个时间点缓存到期,而 Cache-Control 则代表缓存的有效期是多长时间。Expires 设置时间,Cache-Control 设置时长,根据业务场景不同可以使用不同的响应头。

image-20210713162058257

Nginx

介绍

功能:

  • 反向代理
  • 负载均衡
  • 静态资源缓存与压缩功能
静态资源缓存配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
# 设置缓存目录

# levels代表采用1:2也就是两级目录的形式保存缓存文件(静态资源css、js)

# keys_zone定义缓存的名称及内存的使用,名称为babytun-cache ,在内存中开始100m交换空间

# inactive=7d 如果某个缓存文件超过7天没有被访问,则删除

# max_size=20g;代表设置文件夹最大不能超过20g,超过后会自动将访问频度(命中率)最低的缓存文件删除

proxy_cache_path d:/nginx-cache levels=1:2 keys_zone=babytun-cache:100m inactive=7d max_size=20g;

#配置xmall后端服务器的权重负载均衡策略

upstream xmall {

server 192.168.31.181 weight=5 max_fails=1 fail_timeout=3s;

server 192.168.31.182 weight=2;

server 192.168.31.183 weight=1;

server 192.168.31.184 weight=2;

}

server {

#nginx通过80端口提供Web服务

listen 80;

# 开启静态资源缓存

# 利用正则表达式匹配URL,匹配成功的则执行内部逻辑

# ~* 代表URL匹配不区分大小写

location ~* \.(gif|jpg|css|png|js|woff|html)(.*){

# 配置代理转发规则

proxy_pass http://xmall;

proxy_set_header Host $host;

proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

proxy_cache xmall-cache;

#如果静态资源响应状态码为200(成功) 302(暂时性重定向)时 缓存文件有效期1天

proxy_cache_valid 200 302 24h;

#301(永久性重定向)缓存保存5天

proxy_cache_valid 301 5d;

#其他情况

proxy_cache_valid any 5m;

#设置浏览器端缓存过期时间90天

expires 90d;

}



#使用xmall服务器池进行后端处理

location /{

proxy_pass http://xmall;

proxy_set_header Host $host;

proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

}

}

服务端缓存

部署方式分类:进程内缓存,分布式缓存

进程内缓存

介绍:应用中开辟的一块内存空间,数据在运行时被载入这块内存,通过本地内存的低延迟、高吞吐的特性提高程序的访问速度。

应用场景:进程内缓存在众多 Java 框架内都有广泛应用,例如 Hibernate、Mybatis 框架的一二级缓存、Spring MVC 的页面缓存都是进程内缓存的经典应用场景。

开源实现:如 EhCache、Caffeine。

分布式缓存

介绍:对多个服务数据的热点数据进行集中缓存。

实现:基于 Redis 内存型 NoSQL 数据库,对多个应用的热点数据进行集中缓存。

多级缓存

介绍:进程内缓存结合分布式缓存。

优点:性能高,分摊分布式缓存压力。查询相同数据时直接从本地 EhCache 缓存提取,不再产生新的网络通信,应用查询性能得到显著提高。

缺点:复杂,增加数据一致性问题

场景:

  • 缓存的数据是稳定的。例如邮政编码、地域区块、归档的历史数据这些信息适合通过多级缓存减小 Redis 与数据库的压力。
  • 瞬时可能会产生极高并发的场景。例如春运购票、双 11 零点秒杀、股市开盘交易等,瞬间的流量洪峰可能击穿 Redis 缓存,产生流量雪崩。这时利用预热的进程内缓存分摊流量,减少后端压力。
  • 一定程度上允许数据不一致。例如某博客平台中你修改了自我介绍这样的非关键信息,此时在应用集群中其他节点缓存不一致也并不会带来严重影响,对于这种情况我们采用T+1的方式在日终处理时保证缓存最终一致就可以了。
实现

​ 在 Java 应用层面,只有 EhCache 的缓存不存在时,再去 Redis 分布式缓存获取,如果 Redis 也没有此数据再去数据库查询,数据查询成功后对 Redis 与 EhCahce 同时进行双写更新。

image-20210713170806678

数据一致性问题

不同应用EhCache数据一致性问题。

解决:引入 MQ 消息队列,利用 RocketMQ 的主动推送功能来向其他服务实例以及 Redis 缓存服务发起变更通知。

image-20210713171323964

微服务部署

软件部署发展过程

物理机部署

应用程序安装在物理服务器的操作系统中,应用程序直接通过操作系统获取物理服务器的 CPU、内存、硬盘等资源。

缺点:CPU 闲置、内存过剩等资源浪费。物理机价格高

虚拟机部署

通过 VMWare 或者 VirtualBox 等虚拟化工具,可以将高性能物理服务器切割为若干虚拟机,这些虚拟机拥有自己独立的 CPU、内存、硬盘资源,并且这些资源彼此隔离不允许交叉访问。

运维工程师就可以为不同类型的应用分配不同的资源,如计算密集型的应用就多分配一些 CPU 核数,存储密集型应用就多分配一些内存与硬盘空间,并且这些资源可以在不停机的情况下实现动态调整,让服务器资源得到最大化的利用。

容器化部署

通过将应用程序镜像化,再通过镜像直接生成一个个容器实现应用的快速部署发布;同时容器化技术不再强调资源隔离,所有容器底层通过 Docker 容器引擎与操作系统获取全局共享的物理机资源。

优点:

  • 标准化的部署过程。因为容器化关注应用本身,因此创建容器的过程就是部署应用的过程。容器将是标准化的产物,可能容器内部的应用程序功能各不相同,但对运维人员来说创建容器的命令与操作过程都是基本相同的,可以通过脚本快速批量的完成容器的创建。
  • 更好的性能。相比虚拟机,容器化并不强调资源隔离,物理机的所有资源对于容器都是共享的,容器与底层资源之间通过 Docker 容器引擎与操作系统进行调度,这中间产生的损耗相比虚拟机小得多。

软件安装包-容器化技术Docker

单独写TODO

介绍

Docker 是一个开源的应用容器引擎,基于 Go 语言开发。Docker 可以让开发者打包他们的应用以及依赖包到一个轻量级、可移植的容器中,然后发布到任何流行的 Linux 机器上,Docker 经过多年发展已经是容器化技术的标准。

概念

镜像,仓库,Dockerfile,容器,容器编排工具

  • 镜像(Image)

    类似于安装包,可以用安装包安装软件程序。

    可以使用镜像在任何安装了 Docker 的 Linux 系统上快速部署应用程序。

    jar包打成镜像后,可以用镜像在多个服务器快速部署。

  • 仓库

    仓库是存放镜像的地方,Docker 提供了 DockerHub 仓库站托管开发者的镜像文件,开发者可以利用 Pull 命令直接从仓库下载镜像到本地部署。

    生产环境需要自己搭建HARBOR 仓库。如同代码仓库GitLab。

  • Dockerfile

    描述将应用程序构建为镜像的过程。如:将jar构建为镜像。

    运维可以用镜像屏蔽硬件系统差异在不同实例部署多个应用程序。

  • 容器(Container)

    安装包安装成功后是软件(应用程序)。

    镜像部署成功后就是容器,容器是镜像的实例。

  • 容器编排工具

    容器编排工具的典型代表是 Google Kubernetes(K8S) 和 Docker Swarm,容器编排工具用于管理大规模集群中的容器实例。

    服务器网络间自动实现互联互通,随着外部用户的访问压力的变化自动进行容器的扩容与收缩。

    K8S 允许运维人员通过可视化的方式对容器进行动态调整,同时对所有运行节点也提供了实时监控。

    image-20210714122334467

1.20后k8s放弃了docker部分功能,比如网络与volume,国内基本都是1.1x。

DevOps执行流程

image-20210714113050900

执行流程:

  1. 研发工程师将测试验收后的源码上传到 GitLab 服务器,并合并到生成分支(包含 Dockerfile,用来描述 Docker 镜像的构建过程)。
  2. 软件工程师或者配置管理员发起 Jekins 的自动化脚本,完成镜像的自动化构建与仓库推送。
  3. 上线日运维工程师接入 Kubernetes 管理端,发起 Deploy 部署命令,此时生产环境的 K8S 节点会从 HARBOR 仓库抽取最新版本的应用镜像,并在服务器上自动创建容器,最新版本的 Jar 文件在容器创建时也会随之启动开始对外提供服务。
    1. 抽取新版本源码到 Jekins 服务器,利用 Jekins 服务器安装 Maven 自动完成编译、测试、打包的过程;产出jar文件
    2. 抽取 Dockerfile 到 Jekins 服务器,利用 Jekins 服务器安装的 Docker 完成镜像的构建工作,在构建过程中需要将上一步生成的 Jar 文件包含在内,在容器创建时自动执行这个 Jar 文件。
    3. 镜像生成后,还是通过 Jekins 服务器上的 Docker 将新版本镜像推送到 HARBOR 仓库。(HARBOR 用于创建 Docker 镜像的私有仓库)
  4. 在校验无误后,本次上线宣告成功。

真实环境异常因素:

  • 源码编译、打包时产生异常的快速应对机制
  • 上线失败如何快速应用回滚
  • 镜像构建失败的异常跟踪与补救措施

老项目升级为微服务策略

单体应用改造升级为微服务架构。

痛点

分布式事务54%

全链路跟踪43%

服务平滑上下线33%

面临的问题

  • 改造是一步到位还是逐渐迭代?
  • 微服务拆分的粒度是什么?
  • 如何保证数据一致性?
  • 新老交替过程中如何不影响公司业务进展?

策略

  • 严禁 Big Bang(一步到位);
  • 尽早体现价值;
  • 优先分离做前后端;
  • 新功能构建成微服务;
  • 数据源不混用
  • 利用 Spring AOP 开发低侵入的胶水代码;
  • 基于 MQ 构建反腐层。

绞杀式升级

介绍:

​ 一个由微服务组成的新应用程序,通过将新功能作为服务,并逐渐从单体应用中提取服务来实现。随着时间的推移,越来越多单体应用内的功能被逐渐剥离为独立的微服务,最终达到消灭单体应用的目的。

优点:

  • 升级改造过程中并不需要推翻原有的代码,而是在新老更迭的过程中一步步完成微服务架构的升级改造。
  • 立即获得投资回报。

严禁 Big Band

逐步重构单体应用,采用绞杀者应用策略,将应用变为单体与微服务的混合状态,随着时间增加一点点蚕食掉单体应用。

尽早体现价值

按价值的重要性进行排序。

优先分离做前后端

前后端独立部署、扩展与维护。表示层在快速迭代部署时并不影响后端功能,可以轻松进行 A/B 测试。

新功能构建成微服务

例如检索服务。增加了 API Gateway 网关,该网关对前端访问的 URL 进行路由。访问 search 接口,则请求被重定向到新创建的商品检索微服务,通过 ElasticSearch 这种专用的全文检索引擎提供更高级的查询功能;

数据源不混用

可使用 Alibaba Canal 做数据源同步,Canal 是阿里巴巴旗下的开源项目,纯 Java 开发。基于数据库增量日志解析,提供增量数据订阅&消费,可自动实现从 MySQL 数据源向其他数据源同步数据的任务。

旧功能修改

Spring AOP 扩展
1
2
3
4
5
6
7
8
9
10
11
12
@Component("priceServiceAspect") //声明Bean Id
@Aspect //定义切面类
public class PriceServiceAspect{
@Resource
private PriceServiceFeignClient priceServiceFeignClient;
//利用环绕通知实现对PriceService.findByGoodsId的动态代理
@Around("execution(* com.lagou..PriceService.findByGoodsId(..)")
public Object selectGoods(ProceedingJoinPoint joinPoint){
//通过OpenFeign客户端向定价服务发起远程请求,替代JVM本地访问
return priceServiceFeignClient.selectGoods((Long)joinPoint.getArgs()[0]);
}
}
MQ构建反腐层

image-20210713214025802

分布式sql

不使用join而使用in,不同mysql实例不能使用join