image.png

Hello,World.

  • DependencyManagerment 和 Dependencies的区别

    • DependencyManagerment只是声明依赖,并不实现引入。位于 组织或项目最顶层的父pom中。pom.xml中DependencyManagerment元素能让所有在子项目中引用一个依赖而不用显式列出版本号。Maven会沿着父子层次向上走,直到找到一个拥有DependencyManagerment的项目,使用DependencyManagerment元素中指定的版本号。

首说RestTemplate

❔是什么 提供了多种便捷访问远程Http服务的方法,是一种简单便捷的访问restful服务模板类,是Spring提供的用于访问Rest服务的客户端模板工具集。
❓怎么用 使用restTemplate访问restful接口,(url,requestMap,ResponseBean.class)三个参数 分别代表REST请求地址、请求参数、HTTP响应转换被转换成的对象类型。
截屏2022-01-31 下午11.40.31.png截屏2022-01-31 下午11.40.45.png截屏2022-01-31 下午11.40.58.png

Run DashBoard

show in the finder->.idea->workspace.xml

  1. <component name="RunDashboard">
  2. <option name="configurationTypes">
  3. <set>
  4. <option value="SpringBootApplicationConfigurationType" />
  5. </set>
  6. </option>
  7. </component>

工程重构:封装复用代码

新建工程 cloud-api-commons,封装untities包。

  1. <!-- 引入自定义untities包-->
  2. <dependency>
  3. <groupId>at.company.springcloud</groupId>
  4. <artifactId>cloud-api-commons</artifactId>
  5. <version>${project.version}</version>
  6. </dependency>

服务注册与发现

一、Eureka

基础知识

服务治理❓

传统的rpc远程调用框架中,管理每个服务与服务之间依赖关系比较复杂,需要服务治理来管理服务之间的依赖关系,实现服务调用、负载均衡、容错、服务发现与注册等。SpringCloud封装了Netflix公司开发的Eureka模块实现服务治理。

服务注册❓

Eureka采用cs设计架构,Eureka Server作为服务注册功能的服务器,是服务注册中心。系统中其他微服务使用Eureka的客户端连接到Eureka Server并维持心跳连接。维护人员可以通过Eureka Server来监控系统中各个微服务是否正常运行。
服务注册与发现中有一个注册中心。当服务器启动时,会把当前服务器的信息,如服务地址通讯地址等以
别名方式注册到注册中心。另一方(消费者|服务提供者),以该别名的方式去注册中心上获取实际的服务通讯地址,实现本地RPC调用RPC远程调用框架核心设计思想:在于注册中心,因为使用注册中心管理每个服务与服务之间的依赖关系(服务治理)。在任何rpc远程框架中,都会有一个注册中心存放服务地址相关信息(接口地址)。
70AC9E61340BE586541433921101E4A7.png

组件

  1. Eureka Server 提供服务注册。各个微服务节点通过配置启动后,在Eureka Server中注册,Eureka Server的服务注册表中会存储所有可用服务节点信息,服务节点信息可在界面中直观看到。
  2. Eureka Client 通过注册中心进行访问。是一个java客户端,用于简化与Eureka Server的交互。客户端具备一个内置的、轮询(round-robin)负载算法的负载均衡器。应用启动后,将会向Eureka Server发送心跳(默认周期60s)。如果Eureka Server在多个心跳周期内没有接收到某节点的心跳,Eureka Server将会从服务注册表中把这个服务节点移除(默认90s)。

    Eureka Server/client 节点注册

  3. 导入jar包,服务中心导入server,客户端导入client

    1. <dependency>
    2. <groupId>org.springframework.cloud</groupId>
    3. <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    4. </dependency>
    5. <dependency>
    6. <groupId>org.springframework.cloud</groupId>
    7. <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    8. </dependency>
  4. application.yml相关配置 ```xml

    eureka-server

    eureka: instance: hostname: localhost #eureka服务端实例名称 client:

    false表示不向注册中心注册自己

    register-with-eureka: false

    false表示自己是注册中心,职责是维护服务实例,不需要检索服务

    fetch-registry: false service-url:

    设置与eureka server交互的地址查询服务和注册服务

    defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/

eureka-client

不同微服务名称不同

spring: application: name: cloud-payment-service eureka: client:

  1. #true表示向注册中心注册自己
  2. register-with-eureka: true
  3. #单节点无所谓 集群必须设置为true 配合ribbon使用负载均衡 是否从EurekaServer抓取已有的注册信息
  4. fetchRegistry: true
  5. service-url:
  6. # 设置与eureka server交互的地址查询服务和注册服务
  7. defaultZone: http://localhost:7001/eureka/

3. @EnableEurekaServer/@EnableEurekaClient

�![截屏2022-02-01 下午4.26.47.png](https://cdn.nlark.com/yuque/0/2022/png/25505217/1643704011164-d63bdb1a-16d1-419f-859a-52e105d1928f.png#clientId=u21430c68-697c-4&crop=0&crop=0&crop=1&crop=1&from=drop&id=ub232d098&margin=%5Bobject%20Object%5D&name=%E6%88%AA%E5%B1%8F2022-02-01%20%E4%B8%8B%E5%8D%884.26.47.png&originHeight=288&originWidth=1420&originalType=binary&ratio=1&rotation=0&showTitle=true&size=83887&status=done&style=none&taskId=uec066e32-1137-4666-bf33-c4ba7d42279&title=%E7%BA%A2%E8%89%B2%E8%AD%A6%E5%91%8A%E6%98%AF%E4%BF%9D%E6%8A%A4%E6%9C%BA%E5%88%B6 "红色警告是保护机制")
<a name="CwW5z"></a>
### Eureka集群搭建
![D5DB2AD165ADF761A9CBB4A7F02A576C.png](https://cdn.nlark.com/yuque/0/2022/png/25505217/1643704189126-12f7f38f-f125-421f-be4c-f43d542a7433.png#clientId=u21430c68-697c-4&crop=0&crop=0&crop=1&crop=1&from=drop&id=udfd5fc5c&margin=%5Bobject%20Object%5D&name=D5DB2AD165ADF761A9CBB4A7F02A576C.png&originHeight=287&originWidth=913&originalType=binary&ratio=1&rotation=0&showTitle=true&size=138926&status=done&style=none&taskId=ud603507a-e588-453f-92c6-0d92256344a&title=%E5%8E%9F%E7%90%86 "原理")<br />高可用是rpc远程过程调用的核心。<br />**再加一个注册中心server**

1. mac -> sudo vim /private/etc/hosts
   1. 127.0.0.1 eureka7001.com
   2. 127.0.0.1 eureka7002.com
2. 7001/7002
```xml
server:
  port: 7002 #7002server



eureka:
  instance:
    hostname: eureka7002.com   #eureka服务端实例名称
  client:
    #false表示不向注册中心注册自己
    register-with-eureka: false
    #false表示自己是注册中心,职责是维护服务实例,不需要检索服务
    fetch-registry: false
    service-url:
#     设置与eureka server交互的地址查询服务和注册服务
      defaultZone: http://eureka7001.com:7001/eureka/


#7001 server
server:
  port: 7001



eureka:
  instance:
    hostname: eureka7001.com   #eureka服务端实例名称
  client:
    #false表示不向注册中心注册自己
    register-with-eureka: false
    #false表示自己是注册中心,职责是维护服务实例,不需要检索服务
    fetch-registry: false
    service-url:
#     设置与eureka server交互的地址查询服务和注册服务
#相互监管
      defaultZone: http://eureka7002.com:7002/eureka/

截屏2022-02-01 下午5.16.26.png截屏2022-02-01 下午5.16.37.png

  1. 对于client 只需修改配置文件的defaultZone属性

    defaultZone: http://eureka7002.com:7002/eureka/,http://eureka7001.com:7001/eureka/
    
  2. 启动顺序:Server->provider->consumer

再加一个provider

  1. 使用端口号区分不同的provider

    @Value("${server.port}")
     private String serverPort;
    

    截屏2022-02-01 下午5.42.16.png

  2. 回到consumer节点,PAYMARYURL不能写死//
    public static final String _PAYMENT_URL
    = “http://CLOUD-PAYMENT-SERVICE“;

且对于RestTemplate的注册,需要加上@LoadBalanced,负载均衡机制。

  1. 微服务信息完善:配置文件添加属性

    eureka:
     instance: 
    #实现命名规范
     instance-id: payment8002
    # 访问路径可以显示ip地址
          prefer-ip-address: true
    

    服务发现Discovery

    ❓获取注册进eureka中微服务的信息

  2. 在provider(8001)的控制器方法中: ```java //服务发现 @Resource private DiscoveryClient discoveryClient; @GetMapping(value=”/payment/discovery”) public Object discover(){

         List<String> services = discoveryClient.getServices();
         for(String service:services){
    

    // 得到服务清单列表

             log.info("****element:***"+service);
         }
    

    // 一个具体的别名下的所有个体实例

     List<ServiceInstance> instances = discoveryClient.getInstances("CLOUD-PAYMENT-SERVICE");
         for(ServiceInstance instance:instances){
             log.info(instance.getServiceId()+"\t"+instance.getHost()+"\t"+instance.getPort()+"\t"+instance.getUri());
         }
    
         return this.discoveryClient;
    
}
output:<br />****element:***cloud-payment-service<br />****element:***cloud-order-service<br />CLOUD-PAYMENT-SERVICE    192.168.0.105    8001    [http://192.168.0.105:8001](http://192.168.0.105:8001)

2. 在provider结点的中方法中加入 @EnableDiscoveryClient 
<a name="o5ukZ"></a>
### Eureka自我保护机制
EMERGENCY! EUREKA MAY BE INCORRECTLY CLAIMING INSTANCES ARE UP WHEN THEY'RE NOT. RENEWALS ARE LESSER THAN THRESHOLD AND HENCE THE INSTANCES ARE NOT BEING EXPIRED JUST TO BE SAFE.Server首页的以上说明Eureka进入保护模式。<br />❓**保护模式**主要用于一组客户端和Eureka Server之间存在网络分区场景下的保护。一旦进入保护模式,Eureka Server会尝试保护其服务注册表中的信息,不再删除服务注册表中的数据,不会注销任何微服务。某时刻某个微服务不可用,Eureka不会立刻清理,依旧会对该微服务信息进行保护。属于CPA里的AP分支。<br />❓为什么会产生Eureka 自我保护机制<br />防止Eureka Client可以正常运行,但是与Eureka Server网络不同情况下,Eureka Server不会立即将Eureka Client服务剔除。<br />❓什么是自我保护模式<br />默认情况下,Eureka Server一定时间内没有接收到某个微服务的心跳,Eureka Server将会注销该实例(90s)。但当网络分区故障发生(延时、卡顿、拥挤)时,微服务与Eureka Server之间无法正常通信,由于微服务此时本身是健康的,所以不应该注销该微服务。Eureka通过自我保护机制来解决这个问题---Eureka Server结点短时间内丢失过多客户端(可能发生了网络分区故障),节点会进入自我保护模式。<br />🈲 禁止自我保护<br />出厂默认开启自我保护模式。

1. 在Eureka Server的配置文件中更改。
```xml
eureka:

        #关闭自我保护模式
  server:
        enable-self-preservation: false
        #设置心跳检测间隔时间
    eviction-interval-timer-in-ms: 2000
  1. 在EurekaClient的配置文件中
    eureka:
     instance:
         #Eureka客户端向服务器发送心跳的时间间隔,默认30s
     lease-renewal-interval-in-seconds: 1
     #Eureka服务端收到最后一次心跳后等待时间上线 默认90s 超时会剔除服务
     lease-expiration-duration-in-seconds: 2
    

    二、Zookeeper

    分布式协调工具,可以实现注册中心功能。关闭Linux服务器防火墙后启动zookeeper服务器。zookeeper服务器替代Eureka服务器,zk作为服务注册中心。
             <dependency>
             <groupId>org.apache.zookeeper</groupId>
             <artifactId>zookeeper</artifactId>
             <version>3.5.7</version>
             <exclusions>
                 <exclusion>
                     <groupId>org.slf4j</groupId>
                     <artifactId>slf4j-log4j12</artifactId>
                 </exclusion>
             </exclusions>
         </dependency>
    
    微服务模块作为临时结点注册进入zookeeper。

    三、Consul

    开源的分布式服务发现和配置管理系统,Go语言开发。提供了微服务系统中的服务治理、配置中心、控制总线等功能。这些功能每一个都可以根据需要单独使用,也可以共同构建全方位的服务网络,提供了一种完整的服务网格解决方案。
    基于raft协议,支持健康检查,支持HTTP和DNS协议,支持跨数据中心的WAN集群,提供图形界面,跨平台。 ```bash //下载 mac brew tap hashicorp/tap brew install hashicorp/tap/consul // consul版本 consul -v -Consul v1.11.2 -Revision 37c7d06b -Protocol 2 spoken by default, understands 2 to 3 (agent will automatically use protocol >2 when speaking to compatible agents) consul agent -dev //启动consul 启动后浏览http://localhost:8500/ui/dc1/services
<a name="yvkgW"></a>
### CAP📚
一个分布式系统不可能同时满足 一致性Consistency  可用性Available 分区容错性Partition Tolerance。最多只能满足其中两项。<br />一致性:多个副本之间的数据一致的特性<br />可用性:系统提供的服务一直处于可用状态,对于用户的操作请求总是在有限时间内返回结果。<br />分区容错性:分布式系统遇到任何网络分区故障,仍然对外保证提供满足一致性和可用性的服务,除非整个网络都发生故障。<br />AP(Eureka)<br />CP(Zookeeper/Consul
<a name="MOmfB"></a>
# 服务调用
<a name="wDbEv"></a>
## 一、Ribbon
Spring Cloud Ribbon 基于Netflix Ribbon实现的一套客户端负载均衡的工具。<br />Ribbon是Netflix发布的开源项目,主要功能是提供客户端的软件负载均衡算法和服务调用。Ribbon客户端组件提供一些列完善的配置项如连接超时,重试等。简单的说,就是在配置文件中列出Load Balancer后面所有的机器,Ribbon会自动的帮助你基于某种规则(如简单轮询、随机连接)去连接这些机器。我们使用Ribbon实现自定义的负载均衡算法。<br />Ribbon是一个软负载均衡的客户端组件,可以与其他所需请求的客户端结合使用。
<a name="NJqx6"></a>
### Load Balance
将用户的请求平摊的分配到多个服务上,从而达到系统的HA(高可用。常见的负载均衡有软件 Nginx、LVS,硬件F5<br />Ribbon负载均衡 🆚 Nginx负载均衡:前者 本地负载均衡,调用微服务接口时,会在注册中心上获取注册信息服务列表之后缓存到JVM本地,从而在本地实现RPC远程服务调用技术。而后者,是服务器负载均衡,客户端所有请求都会交给nginx,由nginx实现转发请求。负载均衡通过服务端实现。
<a name="XcG2F"></a>
#### 进程内LB
将lb逻辑集成到消费方,消费方从服务注册中心获知可用地址,从这些地址中选取合适的服务器。<br />Ribbon属于进程内LB,只是一个类库,集成于消费方进程,消费方通过它来获取到服务提供方的地址。
<a name="jd6wR"></a>
#### 集中式LB
在服务的消费方和提供方之间使用独立的lb设施(硬件F5或软件nginx),由该设施负责把访问请求通过某种策略转发至服务的提供方。

Ribbon工作时分为两步:

1. 选择Eureka Server,优先选择同一区域内负载较少的。
2. 根据用户指定的策略,从server中取到的服务注册列表中选择一个地址。
3. Ribbon提供了多种策略,轮询、随机、根据响应时间加权。

<a name="QWgES"></a>
### 二说RestTemplate
<a name="qngs1"></a>
#### getForEntity/postForEntity
```java
@GetMapping("/consumer/payment/getForEntity/{id}")
    public CommonResult<Payment> getPaymentById2(@PathVariable("id")Long id){

//        String url, Class<T> responseType, Object... uriVariables
//        return restTemplate.getForObject(PAYMENT_URL+"/payment/get/"+id,CommonResult.class);

        ResponseEntity<CommonResult> entity = restTemplate.getForEntity(PAYMENT_URL + "/payment/get/" + id, CommonResult.class);
        if(entity.getStatusCode().is2xxSuccessful()){
            //返回请求体

            return entity.getBody();


        }else{
            return new CommonResult<>(444,"操作失败");
        }

    }
    @GetMapping("/consumer/payment/postForEntity/create")
    public CommonResult<Payment> create2(Payment payment){
//        url ResultMap ResponseBean.class
//        return restTemplate.postForObject(PAYMENT_URL+"/payment/create",payment,CommonResult.class);
        ResponseEntity<CommonResult> entity = restTemplate.postForEntity(PAYMENT_URL + "/payment/create", payment, CommonResult.class);
        if(entity.getStatusCode().is2xxSuccessful()){
            return entity.getBody();
        }else{
            return new CommonResult<>(444,"操作失败");
        }

    }

IRule算法

package com.netflix.loadbalancer;

public interface IRule {
    Server choose(Object var1);

    void setLoadBalancer(ILoadBalancer var1);

    ILoadBalancer getLoadBalancer();
}

自定义IRule算法

自定义配置类不能放在@ComponentScan所扫描的当前包以及子包下,否则自定义配置会被Ribbon客户端共享,达不到特殊化定制的目的。

//放置在 com.atcompany.myrule包下
@Configuration
public class MySelfRule {

    @Bean
    public IRule myRule(){

        //默认为轮询 自定义为随机
        //其他算法方式替换同理
        return new RandomRule();
    }

}

同时在主启动类添加注解 @RibbonClient(name=”CLOUD-PAYMENT-SERVICE”,configuration = MySelfRule.class) 重启服务器访问服务中心成为随机。

原理(轮询算法

rest接口第n次请求数 % 服务器集群总数 = 实际调用服务器位置下标。
每次服务器重启后rest接口从0开始。


private int incrementAndGetModulo(int modulo) {
      //自旋锁
        int current;
        int next;
        do {
            current = this.nextServerCyclicCounter.get();
            next = (current + 1) % modulo;
        } while(!this.nextServerCyclicCounter.compareAndSet(current, next));

        return next;
    }
    /**
     * Atomically sets the value to the given updated value
     * if the current value {@code ==} the expected value.
     *
     * @param expect the expected value
     * @param update the new value
     * @return {@code true} if successful. False return indicates that
     * the actual value was not equal to the expected value.
     */

 public final boolean compareAndSet(int expect, int update) {
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }

自旋锁spinlock

尝试获取锁的线程不会立即阻塞,而是采用循环的方式尝试获取锁,这样可以减少线程上下文切换的消耗,但循环会消耗CPU。

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;

public class SpinLockDemo {
    //泛型 原子引用线程
    AtomicReference<Thread> atomicReference = new AtomicReference<>();

    /**
     * AA先启动线程 进入mylock 运行compareAndSet
     * BB进入线程 等待AA运行myunlock后(将atomicReference赋值为null)
     * mylock 中的while循环就是自旋锁
     * @param args
     */
    public static void main(String[] args) {

        SpinLockDemo lock = new SpinLockDemo();

        new Thread(()->{
            lock.myLock();
            try {

                TimeUnit.SECONDS.sleep(5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            lock.myunLock();
        },"AA").start();

        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }


        new Thread(()->{
            lock.myLock();
            lock.myunLock();
        },"BB").start();


    }
    public void myLock(){
        //当前线程
        Thread thread = Thread.currentThread();
        System.out.println(Thread.currentThread().getName()+"\t come in");

        //假设当前第一次访问 atomicReference未初始化 是null值 则将thread赋值 同时返回boolean值 atomicReference==null
        while(!atomicReference.compareAndSet(null,thread)){
            //如果atomicReference!=null 返回true 跳出while

        }


    }
    public void myunLock(){
        Thread thread = Thread.currentThread();
        //此时要解锁 atomicReference == thread? null
        atomicReference.compareAndSet(thread,null);
        System.out.println(Thread.currentThread().getName()+"\t invoked myLock()");
    }
}

CAS:compare and set

CPU并发原语,功能是判断内存某个位置的值是否为预期值,即比较工作内存的值和主内存的值。如果是则更改为新的值。
CAS并发原语体现在java语言中是sun.misc.Unsafe类的各个方法。调用Unsafe类的CAS方法,jvm帮我们实现CAS汇编指令,这是一种完全依赖于硬件的功能,通过它实现了原子操作。由于CAS是系统原语,原语属于操作系统用语范畴,由若干指令组成,用于完成某个功能的过程,原语的执行必须连续,且执行过程中不允许被中断。CAS是一条cpu的原子指令,不会造成数据不一致问题。
Unsafe.class CAS核心类 java无法直接访问底层系统 需要通过本地(native方法访问。基于Unsafe.class可用直接操作特定内存的数据。sun.misc.Unsafe,内部方法操作可用像c指针一样操作内存,java中CAS操作的执行依赖于Unsafe类的方法。
Unsafe类的方法都是由native修饰,直接调用操作系统底层资源执行相应任务。
得出结论:原子整型的i++操作在多线程环境下不需要使用synchronized 也能保证安全,是由于Unsafe类的使用。

 // setup to use Unsafe.compareAndSwapInt for updates
    private static final Unsafe unsafe = Unsafe.getUnsafe();
//valueOffset是变量值在内存中的偏移地址,因为Unsafe根据内存偏移地址获取数据
    private static final long valueOffset;

    static {
        try {
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }
//value使用volatile修饰,保证多线程之间的内存可见性。
    private volatile int value;
 public final int getAndIncrement() {
    //底层调用Unsafe类的方法
        return unsafe.getAndAddInt(this, valueOffset, 1);
    }

⚓️step into
靠Unsafe类实现原子性,靠自旋锁实现底层思想

                            //(this, valueOffset, 1)
public final int getAndAddInt(Object var1, long var2, int var4) {
    //var1 当前对象 var2 偏移量 var4 需要变动的数量
    //自旋锁
    int var5;
        do {
            var5 = this.getIntVolatile(var1, var2);
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
//该对象当前值与var5比较
    //相同 更新值为var5+var4 返回true
    //不相同 继续取值 比较 直到更新完成
        return var5;
    }
  1. 线程A和B同时执行getAndAddInt(),分别跑在不同的CPU
  2. AtomicInteger的初始值为3,主(物理)内存AtomicInteger的value是3,根据JMM模型,线程A和线程B各自持有一份价值为3的value的副本到自己的工作内存。
  3. 线程B通过getIntVolatile(var1,var2)获取到value值3,此时刚好线程B没有被挂起并执行compareAndSwapInt(),比较内存值也为3,成功修改内存值为4,线程B结束。
  4. 这是线程A恢复,执行CompareAndSwapInt(),比较自己工作区的值3和主内存的值4不同,说明该变量已被其他线程抢先修改,线程A本次修改失败,需要重新读取,再次循环。
  5. 线程A重新获取value,value被volatile修饰,因此其他线程对于value的修改可见,线程A继续执行compareAndSwapInt进行比较替换,直到成功。

❌缺点 :

  1. 循环时间长,开销大。失败会一直尝试。
  2. 只能保证一个共享变量的原子操作
  3. ABA问题

    ABA问题

    狸猫换太子。 CAS算法实现一个重要前提需要取出内存中某时刻的数据并在当下时刻比较并替换,在这个时间差会导致数据的变化。比如线程one从内存位置V中取出A,此时另一个线程two也从内存中取出A,并且线程two进行一定操作将指值变为B,线程two又将V位置的数据变成A,此时线程one进行CAS操作发现内存中仍然是A,线程one开始操作。尽管线程one的操作成功,但不代表这个过程是没有问题的。
    🗝 解决ABA问题
    普通原子引用AtomicReference的构造器: ```java public AtomicReference(V initialValue) {
     value = initialValue;
    
    }
AtomicStampedReference的构造器:
```java
 public AtomicStampedReference(V initialRef, int initialStamp) {
        pair = Pair.of(initialRef, initialStamp);
    }

原子引用+版本号来解决ABA问题

package com.atcompany.springcloud.test;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.atomic.AtomicStampedReference;

public class CASTest {

        //普通原子引用
        static AtomicReference<Integer> atomicReference = new AtomicReference<>(100);

        static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(100,1);
    public static void main(String[] args) {


        System.out.println("----create the ABA------");


        new Thread(()->{
            atomicReference.compareAndSet(100,101);
            atomicReference.compareAndSet(101,100);
        },"t1").start();


        new Thread(()->{

//t2线程暂停1s,保证t1线程完成一次ABA操作
            try {
                TimeUnit.SECONDS.sleep(1);
                System.out.println(atomicReference.compareAndSet(100, 2022)+"\t"+atomicReference.get());

            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        },"t1").start();

        System.out.println("----solve the ABA------");
        new Thread(()->{
            //获取版本号

            int stamp = atomicStampedReference.getStamp();
            System.out.println(Thread.currentThread().getName()+"\t第1次版本号:"+stamp);
            try {
                TimeUnit.SECONDS.sleep(1);
                System.out.println(atomicReference.compareAndSet(100, 2022)+"\t"+atomicReference.get());

            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            atomicStampedReference.compareAndSet(100,101,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);
            System.out.println(Thread.currentThread().getName()+"\t第2次版本号:"+stamp);
            atomicStampedReference.compareAndSet(100,101,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);
            System.out.println(Thread.currentThread().getName()+"\t第3次版本号:"+stamp);

        },"t3").start();

        new Thread(()->{

            int stamp = atomicStampedReference.getStamp();
            System.out.println(Thread.currentThread().getName()+"\t第1次版本号:"+stamp);
            try {

                //休息3s 保证t3线程进行一次ABA操作
                TimeUnit.SECONDS.sleep(3);
                System.out.println(atomicReference.compareAndSet(100, 2022)+"\t"+atomicReference.get());
                 //是否修改成功
                boolean result = atomicStampedReference.compareAndSet(100, 2022, stamp, stamp + 1);
              //最新版本号
                int newStamp = atomicStampedReference.getStamp();
                System.out.println(Thread.currentThread().getName()+"\t修改成功否:"+result+newStamp);
                System.out.println("当前最新值:"+atomicStampedReference.getReference());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
      },"t4").start();
   }
}

JMM(java内存模型

java memory model 本身是一个抽象概念,不真实存在,描述的是一组规则或规范,定义了程序中各个变量(包括实例字段、静态字段、构成数组对象的元素)的访问方式。
JMM关于同步的规定:

  1. 线程解锁前,必须把共享变量的值刷新回主内存
  2. 线程加锁前,必须读取主内存的最新值到自己的工作内存
  3. 加锁解锁 是同一把锁

JVM运行程序的实体是线程,每个线程创建时JVM都会为其创建一个工作内存(栈空间),工作内存是每个线程的私有数据区域,而Java内存模型规定所有变量都存储在主内存,主内存是共享内存区域,所有线程可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝到自己的工作内存,然后对变量进行操作,操作后将变量写回主内存,不能直接操作主内存中的变量,各个线程中的工作内存中存储着主内存的变量副本拷贝,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存完成。
截屏2022-02-06 下午4.58.57.png

手写轮询算法

package com.atcompany.springcloud.lb;

import org.springframework.cloud.client.ServiceInstance;

import java.util.List;

public interface LoadBalancer {
    //得到所有ServiceInstance
    ServiceInstance instances(List<ServiceInstance> serviceInstances);
}
package com.atcompany.springcloud.lb.impl;

import com.atcompany.springcloud.lb.LoadBalancer;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.stereotype.Component;

import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;

@Component
public class MyLB implements LoadBalancer {
//实现接口
    private AtomicInteger atomicInteger = new AtomicInteger(0);

    public final int getAndIncrement(){
        int current;
        int next;
        do{
            current = this.atomicInteger.get();
            next = current>=Integer.MAX_VALUE? 0:current+1;

        }while(this.atomicInteger.compareAndSet(current,next));
        System.out.println("*****next:"+next);
        return next;

//juc(CAS+自旋锁
    }

    @Override
    public ServiceInstance instances(List<ServiceInstance> serviceInstances) {
        int index = getAndIncrement() % serviceInstances.size();

        return serviceInstances.get(index);

    }
}
//在controller方法中 
@Resource
    private LoadBalancer lb;

    @Resource
    private DiscoveryClient client;

    @GetMapping(value="/payment/lb")
    public String getPaymentLB(){
        //通过ClientDiscovery获取所有可用的ServiceInstance组成列表
        List<ServiceInstance> instances = client.getInstances("CLOUD-PAYMENT-SERVICE");
        if(instances==null||instances.size()<=0){
            return null;
        }
        ServiceInstance serviceInstance = lb.instances(instances);
        //传入LoadBalancer作为参数
        URI uri = serviceInstance.getUri();
        return restTemplate.getForObject(uri+"/payment/lb",String.class);

    }

二、OpenFeign

是什么❓Feign是声明式WebService客户端。定义一个服务接口,添加注解。Feign支持可拔插式的编码器和解码器。springcloud对Feign进行了封装,使其支持了Springmvc标准注解和HttpMessageConverters.Feign可以与Eureka和Ribbon组合使用以支持负载均衡。
能干嘛🔆在使用 Ribbon+RestTemplate 时,利用RestTemplate对http请求的封装处理,形成了一套模板化的调用方法。实际开发中,对服务依赖的调用可能不止溢出,往往一个接口会被多处调用,通常会针对每个微服务自行封装一些客户端类来包装这些依赖服务的调用。所以,Feign在此基础上做了进一步封装,帮助定义和实现依赖服务接口的定义。在Feign的实现下,我们只需要创建一个接口并使用注册的方式来配置它(以前是Dao接口上标注mapper注解,现在是一个微服务接口上面标注一个Feign注解),即可完成对服务提供方的接口绑定,简化了使用 spring cloud Ribbon,自动封装服务调用客户端的开发量。
利用Ribbon维护了Payment的服务列表信息,并且通过轮询实现了客户端的负载均衡。与Ribbon不同的是,通过feign只需要定义服务绑定接口并以声明式的方法,优雅而简单的实现服务调用。
对于业务类:

  1. 业务逻辑接口 + @FeignClient 配置调用provider服务
  2. 新建PaymentFeignService接口,新增注解@FeignClient(主启动类添加 @EnableFeignClient
  3. 控制层controller

位于provider模块的控制器方法:

    @GetMapping(value="/payment/feign/timeout")
    public String paymentFeignTimeout(){
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return serverPort;

    }
    @GetMapping(value="/payment/get/{id}")
    public CommonResult getPaymentById(@PathVariable("id")Long id){

        Payment result = pservice.getPaymentById(id);
        log.info("插入结果"+result);


        if(result!=null){
            return new CommonResult(result,"查询数据库成功serverPort"+serverPort,200);
        }else{
            return new CommonResult(null,"查询数据库失败serverPort"+serverPort,444);
        }
    }

面向接口编程:

@Component
//value是eureka上的微服务模块名称
@FeignClient(value="CLOUD-PAYMENT-SERVICE")
public interface PaymentFeignService {

    @GetMapping(value="/payment/get/{id}")
    CommonResult<Payment> getPaymentById(@Param("id")Long id);

      @GetMapping(value="/payment/feign/timeout")
    public String paymentFeignTimeout();


}

openfeign超时控制

对应的控制器方法:

@RestController
@Slf4j
public class OrderFeignController {

    @Resource
    private PaymentFeignService service;


    @GetMapping(value="/consumer/payment/get/{id}")
    CommonResult<Payment> getPayment(@PathVariable("id")Long id){
        return service.getPaymentById(id);
    }
//openfeign 默认等待1s中,超时会报错。 因此下面的方法运行超时
  @GetMapping(value="/consumer/payment/feign/timeout")
    public String timeout(){
        return  service.paymentFeignTimeout();
  }
}

解决超时问题:配置文件的更改

# 设置feign客户端超时时间
ribbon:
#  建立连接所用的时间,网络状况正常时两端连接所用时间
  ReadTimeout: 5000
  # 建立连接后从服务器读取到可用资源所用的时间
  ConnectTimeout: 5000

openfeign日志打印

日志级别

  • NONE: 默认,不显示任何日志
  • BASIC: 仅记录请求方法、URL、响应状态码及执行时间
  • HEADERS: 除了BASIC中定义的信息外,还有请求和响应的头信息
  • FULL:除了HEADERS定义的信息外,还有请求和响应的正文以及元数据

在bean中配置

package com.atcompany.springcloud.config;

import feign.Logger;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class FeignConfig {

        @Bean
    Logger.Level feignLoggerLevel(){
            //配置日志信息
            return Logger.Level.FULL;
        }

}

配置文件配置内容

logging:
  level:
    #feign日志监控哪个接口 什么级别
    com.atcompany.springcloud.service.PaymentFeignService: debug

服务降级

一、Hystrix

服务雪崩

多个微服务之间的调用,假设微服务A调用微服务B和微服务C,微服务B和微服务C又调用其他微服务,这就是扇出。如果扇出的链路上某个微服务的调用响应时间过长或不可用,对微服务A的调用会占用越来越多的系统资源,进而引起系统崩溃,产生雪崩效应。
对于高流量应用来说,单一的后端依赖可能会导致所有服务器上的所有资源都在几秒钟内饱和。这些应用可能导致服务之间延迟增加,备份队列,线程和其他系统资源紧张,导致整个系统发生更多的级联故障。这些都需要对故障和延迟进行隔离和管理,以便单个依赖关系的失败不会取消整个应用程序或系统。
当一个模块某个实例失败后,这个模块还接收流量,有问题的模块还调用其他模块,就发生了级联故障,即雪崩。
是什么❓用于处理分布式系统的延迟和容错的开源库,在分布式系统里,许多依赖不可避免的会调用失败,比如超时、异常等,Hystrix能保证在一个依赖出问题的情况下,不会导致整体服务失败,避免级联故障,以提高分布式系统的弹性。
“断路器“本身是开关装置,当某个服务单元发生故障之后,通过断路器的故障监控(类似于熔断保险丝),向调用方返回一个符合预期的、可处理的备选响应(FallBack),而不是长时间的等待或抛出调用方无法处理的异常,这样保证了服务调用方的线程不会被长时间、不必要的占用,从而避免了故障在分布式系统的蔓延,乃至雪崩。
🙈

服务降级

向调用方返回一个符合预期的、可处理的备选响应(FallBack),而不是长时间的等待或抛出调用方无法处理的异常
1.程序运行异常 2.超时 3.服务熔断触发服务降级 4. 线程池/信号量打满
对服务提供商进行服务降级配置:

一、使用@CommandHystrix指定方法进行降级处理

package com.atcompany.springcloud.service;

import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixProperty;
import org.springframework.stereotype.Service;

import java.util.concurrent.TimeUnit;

@Service
public class PaymentService {

    public String paymentInfo_ok(Integer id){
        return "线程池:"+Thread.currentThread().getName()+"paymentInfo_ok,id:"+id+"\t";
    }
//@HystrixCommand的正常使用需要在主启动类加上@EnableCircuitBreaker激活
    @HystrixCommand(fallbackMethod="paymentInfo_TimeOutHandler",commandProperties={
            //规定3s内是正常业务逻辑 峰值3s
            @HystrixProperty(name="execution.isolation.thread.timeoutInMilliseconds",value="3000")
    })
    public String paymentInfo_error(Integer id){

        try {
            //业务类中却让休眠5s 会出错超时
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        return "线程池:"+Thread.currentThread().getName()+"paymentInfo_error,id:"+id+"\t";
    }

    public String paymentInfo_TimeOutHandler(Integer id){
        //fallback
        //无论是超时还是报错 都会用此方法兜底
        //超时:业务逻辑所用时间超过规定时间 超时不可用
        //报错:如 12/0 
        return "线程池:"+Thread.currentThread().getName()+"paymentInfo_TimeOutHandler,id:"+id+"\t";

    }
}

对consumer进行服务降级配置:设置ribbon和hystrix的超时时间 尽量保持一致


ribbon:
  #  建立连接所用的时间,网络状况正常时两端连接所用时间
  ReadTimeout: 6000
  # 建立连接后从服务器读取到可用资源所用的时间
  ConnectTimeout: 6000

hystrix:
  command:
    default: #也可以针对多个服务
      execution:
        isolation:
          thread:
            timeoutInMilliseconds: 6000 # 设置hystrix的超时时间为4000ms

主配置类添加 @EnableHystrix
控制器方法配置(对于commandProperties的修改,热部署可能不能及时检测,尽量自启动):

    @HystrixCommand(fallbackMethod = "paymentInfo_errorHanlder",commandProperties ={
            @HystrixProperty(name="execution.isolation.thread.timeoutInMilliseconds",value="1500")
    })
    @GetMapping(value="/consumer/payment/hystrix/error/{id}")
    String paymentInfo_error(@PathVariable("id") Integer id){

        String result = service.paymentInfo_error(id);
        return result;

    }


    String paymentInfo_errorHanlder(@PathVariable("id") Integer id){
        return "消费者80,对方支付繁忙,请稍后重试..."
    }

如果每个控制器方法都配有兜底熔断器方法,会造成代码膨胀。

二、使用@CommandHystrix+@DefaultProperties(defaultFallback=””)默认方法进行降级处理

使用该注解统一跳转到统一处理结果页面,避免代码膨胀。

package com.atcompany.springcloud.controller;

import com.atcompany.springcloud.service.PaymentHystrixService;
import com.netflix.hystrix.contrib.javanica.annotation.DefaultProperties;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixProperty;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;
@DefaultProperties(defaultFallback = "payment_Global_FallbackMethod")
@Slf4j
@RestController
public class PaymentHystrixController {
    @Resource
    private PaymentHystrixService service;


//    @HystrixCommand(fallbackMethod = "paymentInfo_errorHanlder",commandProperties ={
//            @HystrixProperty(name="execution.isolation.thread.timeoutInMilliseconds",value="45000")
//    })
    @HystrixCommand
    @GetMapping(value="/consumer/payment/hystrix/error/{id}")
    String paymentInfo_error(@PathVariable("id") Integer id){

        String result = service.paymentInfo_error(id);
        return result;

    }


    String paymentInfo_errorHanlder(@PathVariable("id") Integer id){
        return "消费者80,对方支付繁忙,请稍后重试...";
    }

    //全局fallback方法
    public String payment_Global_FallbackMethod(){
        return "稍后再试~";
    }

}

三、实现service接口来进行降级处理

防止熔断器和业务逻辑混乱:

feign:
  hystrix:
    #开启feign的hystrix支持 默认是false
    enabled: true

接口的注解属性配置


@FeignClient(value="CLOUD-PROVIDER-HYSTRIX-PAYMENT",fallback = PaymentFallbackService.class)

接口实现类:

package com.atcompany.springcloud.service;

import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
@Component
public class PaymentFallbackService implements PaymentHystrixService{

    @Override
    public String paymentInfo_ok(Integer id) {
        return "---PaymentFallbackService fall back --paymentInfo_ok.!.";
    }

    @Override
    public String paymentInfo_error(Integer id) {
        return "---PaymentFallbackService fall back --paymentInfo_error.!.";
    }
}

写在controller类内部的处理,加上默认的或指定的降级处理。如果没有继承service的降级处理,处理的是服务端和客户端的异常。写在controller类内部的处理,并加上默认的或指定的降级处理,如果存在一个继承service的奖及处理,处理的是客户端自身的异常。继承service的降级处理,处理的是服务端的异常。两种降级处理都能处理服务端异常,但是service优先处理服务端异常。

服务熔断

达到最大服务访问直接拒绝访问,然后调用服务降级返回友好提示。服务降级->服务熔断->恢复调用链路。

熔断机制

应对雪崩效应的一种微服务链路保护机制,当扇出链路的某个微服务不可用或响应时间太长,会进行服务降级,进而熔断该节点微服务的调用,快速返回错误的响应信息。当检测到该节点微服务调用响应正常后,恢复调用链路。
springcloud中通过hystrix实现熔断机制。hystrix会监控微服务间调用的状况,当失败的调用到一定阈值,缺省是5s内20次调用失败,会启动熔断机制。注解是@CommandHystrix.
service类中配置:

    //=======服务熔断
    @HystrixCommand(fallbackMethod = "paymentCircuitBreaker_fallback",commandProperties = {
            //是否开启断路器
            @HystrixProperty(name = "circuitBreaker.enabled",value="true"),
            //请求次数
            @HystrixProperty(name = "circuitBreaker.requestVolumeThreshold",value="10"),
            //时间窗口期
            @HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds",value="10000"),
            //失败达到多少后跳闸
            @HystrixProperty(name = "circuitBreaker.errorThresholdPercentage",value="60"),
    })
//在10s的时间内 统计诗词请求 请求失败率达到60%  触发熔断 需要十秒时间恢复
    public String paymentCircuitBreaker(@PathVariable("id") Integer id){
        if(id<0){
            throw new RuntimeException("*****id不能为负数");
        }
        //hutool
        String serialNumber = IdUtil.simpleUUID();
        return Thread.currentThread().getName()+"\t"+"调用成功,流水号:"+serialNumber;
    }


    public String paymentCircuitBreaker_fallback(@PathVariable("id")Integer id){
        return "id不能为负数,请稍后再试~~ id:"+id;
    }

控制器配置:

   //----服务熔断
    @GetMapping(value="/payment/circuit/{id}")
    public String paymentCircuitBreaker(@PathVariable("id")Integer id){
        return service.paymentCircuitBreaker(id);
    }

当达到服务熔断时,正确的访问不会显示出来,需要等待一定时间(时间窗口期sleepWindowInMilliseconds
�,开->半开->关闭。

  • 熔断打开。请求不再调用当前服务,内部设置时钟一般为MTTR(平均故障处理时间),当打开时长达到所设时钟则进入半熔断状态
  • 熔断半开。部分请求根据规则调用当前服务,如果请求成功且符合规则则认为当前服务恢复正常,关闭熔断。
  • 熔断关闭。不会对服务进行熔断。

涉及到短路器的三个重要参数:快照时间窗requestVolumeThreshold、请求总数阀值、错误百分比阀值errorThresholdPercentage。
快照时间窗:断路器确定是否打开需要统计一些错误和请求数据,而统计的时间范围就是快照时间窗,默认为最近10s。
请求总数阀值:在快照时间窗内,必须满足请求总数阈值才有资格熔断。默认20,意味着在10s内,如果该hystrix命令的调用次数不足20次,即使所有的请求都超时或其他原因失败,断路器都不会打开。
错误百分比阀值:当请求总数在快照时间窗内超过了阈值,比如发生了30次调用,在这30次调用中,有15次发生了超时异常,也就是超过了50%的错误百分比,在默认设定50%的阀值情况下,断路器打开。

调用失败会触发降级,而降级会调用fallback方法。无论如何降级的流程是先调用正常方法再调用fallback方法,假如单位时间内调用失败次数过多,就是降级次数过多,则触发熔断,熔断以后会跳过正常方法直接调用fallback方法,所谓”熔断后服务不可用”就是因为跳过正常方法而直接执行fallback。降级是思想,熔断是对降解的具体实现,但是降级的实现不止熔断这一种。

服务限流

秒杀、高并发等操作,排队有序进行。

Hystrix工作流程

image.png image.png

Hystrix Dashboard

除了隔离依赖服务的调用外,Hystrix还提供了准实时的调用监控,Hystrix会持续的记录所有通过hystrix发起的请求的执行信息,并以统计报表和图形的形式展示给用户,包括每秒执行多少请求,多少成功多少失败等。Nextflix通过hystrix-metrics-event-stream项目实现了对以上指标的监控。springcloud也提供了Hystrix Dashboard的整合,对监控内容转化为可视化界面。
进行监控的主类添加注解:@EnableHystrixDashboard

     /**
     * 此配置是为了服务监控而配置,与服务容错本身无关,springcloud升级后的坑
     * ServletRegistrationBean因为SpringBoot的默认路径不是 “/hystrix.stream"
     * 只要在自己的项目里配置上下的servlet就可以了
     */
    @Bean
    public ServletRegistrationBean getServlet() {
        HystrixMetricsStreamServlet streamServlet = new HystrixMetricsStreamServlet() ;
        ServletRegistrationBean registrationBean = new ServletRegistrationBean(streamServlet);
        registrationBean.setLoadOnStartup(1);
        registrationBean.addUrlMappings("/hystrix.stream");
        registrationBean.setName("HystrixMetricsStreamServlet");
        return  registrationBean;
    }

同时需要在dashboard监控模块的配置文件增加,否则为unable to connect hystrix command…

#暴露全部的监控信息
management:
  endpoints:
    web:
      exposure:
        include: "*"

在hystrix监控页面填写网址:� http://localhost:8001/hystrix.stream

服务网关

一、gateway

在Spring生态系统之上构建的API网关服务,基于Spring5,Springboot2,ProjectReactor.旨在提供一种简单而有效的方式对API进行路由,提供一些强大的过滤器功能,如 熔断、限流、重试等。为了提高网关性能,Springcloud Gateway是基于WebFlux框架实现,而WebFlux框架底层使用了高性能的Reactor模式通信框架Netty。基于异步非阻塞模型进行开发。
🚀 动态路由:能匹配任何请求属性 🚀 可以对路由指定Predicate断言和Filter过滤器 🚀 继承hystrix的断路器 🚀 继承springcloud的服务发现 🚀 易于编写的Predicate断言和Filter过滤器 🚀 请求限流功能 🚀 支持路径重写
🆚 Zuul 采用Tomcat容器 使用传统Servlet io处理模型。
servlet由servlet container进行声明周期管理。

  • container启动时 构造servlet对象,并调用serrvlet.init()进行初始化
  • container运行时接收请求,并为每个请求分配一个线程(一般从线程池获取空闲线程),然后调用service()
  • container关闭时调用servlet.destory()销毁servlet。

当请求进入servlet container,servlet container会为其绑定一个线程,在并发不高的场景下这种模型使用。一旦高并发(如用jemeter压),线程数量会上涨,而线程资源代价昂贵,上下文切换内存消耗大,严重影响请求的处理时间。在一些简单业务场景下,不希望为每个request分配一个线程,只希望1个或几个线程就能应对极大并发的请求,这种业务场景下servlet模型没有优势。

三大核心概念

路由Route

构成网关的基本模块,由ID、目标URI、一系列断言和过滤器组成,断言为true则匹配该路由。

断言Predicate

java.util.function.Predicate 匹配http请求中所有内容(包括请求头或请求参数),如果请求和断言相匹配进行路由。

过滤 Filter

指Spring框架中gatewayFilter的实例,使用过滤器,可以在请求被路由前或者之后对请求进行修改。

工作流程

image.png image.pngimage.png

入门配置

配置文件进行路由配置

server:
  port: 9527


spring:
  application:
    name: cloud-gateway
  cloud:
    gateway:
      routes:
        - id: payment_routh     #路由id 要求唯一 建议配合服务名
          uri: http://localhost:8001  #匹配后提供服务的路由地址
          predicates:
            - Path=/payment/get/**    #断言 路径相匹配的进行路由

        - id: payment_routh2  
          uri: http://localhost:8001
          predicates:
            - Path=/payment/lb/**

eureka:
  instance:
    hostname: cloud-gateway-service
  client: # 服务提供者provider注册进eureka服务列表
    service-url:
      register-with-eureka: true
      fetch-registry: true
      defaultZone: http://eureka7001.com:7001/eureka

网关(9527的 pom.xml不要引入web依赖

<!--        <dependency>-->
<!--            <groupId>org.springframework.boot</groupId>-->
<!--            <artifactId>spring-boot-starter-web</artifactId>-->
<!--        </dependency>-->
<!--        <dependency>-->
<!--            <groupId>org.springframework.boot</groupId>-->
<!--            <artifactId>spring-boot-starter-actuator</artifactId>-->
<!--        </dependency>-->

配置成功后网关也可以正常访问 ,不再暴露真实端口。

编码方式进行路由配置

@Configuration
public class GateWayConfig {


    @Bean
    public RouteLocator customeRouteLocator(RouteLocatorBuilder builder){

        RouteLocatorBuilder.Builder routes = builder.routes();
                    //配置的id 
        routes.route("path_route_atcompany",
                r->r.path("/guonei")
                        //访问/guonei会转发到uri中配置的网址
                .uri("http://news.baidu.com/guonei"))
                .build();

        return routes.build();


    }

通过微服务名实现动态路由

默认情况下Gateway会根据注册中心注册的服务列表,以注册中心上微服务名为路径创建动态路由进行转发,从而实现动态路由。

server:
  port: 9527


spring:
  application:
    name: cloud-gateway
  cloud:
    gateway:
      discovery:
        locator:
          enabled: true #开启从注册中心动态创建路由的功能,利用微服务名进行路由
      routes:
        - id: payment_routh     #路由id 要求唯一 建议配合服务名
#          uri: http://localhost:8001  #匹配后提供服务的路由地址
          uri: lb://cloud-payment-service
          predicates:
            - Path=/payment/get/**    #断言 路径相匹配的进行路由

        - id: payment_routh2
#          uri: http://localhost:8001
          uri: lb://cloud-payment-service
          predicates:
            - Path=/payment/lb/**

eureka:
  instance:
    hostname: cloud-gateway-service
  client: # 服务提供者provider注册进eureka服务列表
    service-url:
      register-with-eureka: true
      fetch-registry: true
      defaultZone: http://eureka7001.com:7001/eureka

配置文件的uri没有写死,可以通过网关9527访问。

Predicate

通过参数实现一组匹配规则,让请求找到对应的Route进行处理

spring:
    cloud:
        gateway:
            routes:
                predicates:
            - Path=/payment/lb/**
            - After=2022-02-07T21:57:12.186+08:00[Asia/Shanghai]      # ZonedDateTime.now();

Before和Between与After类似,若两个utc时间,用逗号分隔。

spring:
    cloud:
        gateway:
            routes:
                predicates:
                        - Path=/payment/lb/**
            - Cookie=username,xiaoli

Cookie Route Predicate的两个参数Cookie name ,和 正则表达式。路由规则通过获取对应的Cookie name值和正则表达式去匹配,如果匹配上就会执行路由,不匹配则不执行。
cmd命令行(postman)使用 curl http://localhost:9527/payment/lb —cookie “username=xiaoli”

spring:
    cloud:
        gateway:
            routes:
                predicates:
                        - Path=/payment/lb/**
#请求头含有 X-Request-Id的属性,且值为整数
            - Header=X-Request-Id,\d+

两个参数: 属性名称,正则表达式。属性名和正则表达式匹配则执行。
curl http://localhost:9527/payment/lb -H “X-Request-Id:123”

spring:
    cloud:
        gateway:
            routes:
                predicates:
                        - Path=/payment/lb/**
                        - Host=www.atcompany.com

curl http://localhost:9527/payment/lb -H “Host:www.atcompany.com”

spring:
    cloud:
        gateway:
            routes:
                predicates:
                        - Path=/payment/lb/**
                        - Method=GET
spring:
    cloud:
        gateway:
            routes:
                predicates:
                        - Path=/payment/lb/**
                        - Query=username,\d+

curl http://localhost:9527/payment/lb?username=31

Filter

路由过滤器可用于修改进入的http请求和返回的http响应。路由过滤器只能指定路由进行使用。springcloud gateway内置多种路由过滤器,都在GatewayFilter的工厂类产生。


/**
 * 全局过滤uname属性为null的用户
 * ?uname=xxx
 */
@Component
@Slf4j
public class MyGatewayFilter implements GlobalFilter, Ordered {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        log.info("*****come in MyGatewayFilter:"+new Date());
        String uname = exchange.getRequest().getQueryParams().getFirst("uname");

        if(uname==null){
            log.info("用户名为空,非法名称");
            exchange.getResponse().setStatusCode(HttpStatus.NOT_ACCEPTABLE);
            return exchange.getResponse().setComplete();
        }



        return chain.filter(exchange);
    }

    //表示顺序
    @Override
    public int getOrder() {
        return 0;
    }
}

服务配置

一、springcloud config 分布式配置中心

微服务意味着将单体应用中的业务拆分成为一个个子服务,每个服务粒度相对较小,因此系统中出现大量服务。由于服务都需要必要的配置信息才能运行,所以一套集中式的、动态的配置管理设施必不可少。
是什么❓ springcloud config为微服务架构中的微服务提供集中化的外部配置支持,配置服务器为各个不同微服务应用的所有环境提供了一个中心化的外部配置。
image.png
springcloud config分为服务端和客户端。
服务端也称为分布式配置中心,是独立的微服务应用,用来连接配置服务器并为客户端提供获取配置信息、加密/解密信息等访问接口。
客户端是通过指定的配置中心来管理应用资源,以及与业务相关的配置内容,并在启动的时候从配置中心获取和加载配置信息。配置服务器默认采用git来存储配置信息,有助于对环境配置进行版本管理,并且可以通过git客户端工具来方便的管理和访问配置内容。
🏇 集中管理配置文件 🏇 不同环境不同配置,动态化的配置更新,分环境部署/dev/prod/test/beta/release 🏇 运行期间动态调整配置,不再需要在每个服务部署的机器上编写配置文件,服务会向配置中心统一拉取配置自己的信息🏇 当配置发生变动,服务不需要重启即可感知到配置的变动并应用新的配置 🏇 将配置信息以rest接口的形式暴露 。

服务端配置与绑定

使用微服务3344模块尝试从gitlab上获取内容。
需要进入/etc/hosts 添加映射地址 127.0.0.1 config-3344.com

server:
  port: 3344

spring:
  application:
    name: cloud-config-center
  cloud:
    config:
      server:
        git:
# 使用http方式
          uri: https://gitee.com/Zerlina-ysl/xiaoli-springcloud-config.git
          search-paths:
            - xiaoli-springcloud-config
#公共仓库不需要添加username 和password
#注意 gittee的主分支名称可能是main
      label: main

eureka:
  client:
    service-url:
      default-zone: http://localhost:7001/eureka

截屏2022-02-08 下午1.00.41.png
http://config-3344.com:3344/main/config-dev.yml
/{label}/{application}-{profile}.yml
/{application}-{profile}.yml 默认读取主分支
/{application}-{profile}.yml/{label}

客户端配置与绑定

application.yml 是用户级的资源配置项。 bootstrap.yml的系统级的资源配置项,优先级更高。
spring cloud会创建一个bootstrap context,作为spring应用的application context的父上下文。初始化时,bootstrap context负责从外部源加载配置属性并解析配置。两个上下文共享一个外部获取的environment。
boostrap属性有高优先级,默认情况下不会被本地配置覆盖。boostrap context和application context有不同的约定,新增一个bootstrap.yml,保证两个上下文配置的分离。
要将config-client模块的配置文件改为bootsrtrap.yml,它优先加载,优先级高。

server:
  port: 3355

spring:
  application:
    name: config-client
  cloud:
    config:
      label: main
      name: config  #配置文件名称
      profile: dev    #读取后缀名称 main分支的config-dev
      uri: http://localhost:3344  #配置中心地址

eureka:
  client:
    service-url:
      defaultZone: http://eureka7001.com:7001/eureka

config客户端依赖

<dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-config</artifactId>
        </dependency>

控制器方法


@RestController
public class ConfigClientController {

//获取配置文件的config.info属性值
    @Value("${config.info}")
    private String configInfo;

    @GetMapping("/configInfo")
    public String getConfigInfo(){
        return configInfo;
    }
}

访问http://localhost:3355/configInfo成功实现config客户端3355访问springcloud config3344通过github获取配置信息。
当git中存储的文件改变信息,不能及时热更新,需要重启,只刷新无法获取最新更改。即更改一次git需要重新启动config的客户端和服务端,存在分布式配置的动态刷新问题。

  1. 3355模块的配置文件增加设置

    #暴露监控端点
    managemant:
    endpoints:
     web:
       exposure:
         include: "*"
    
  2. config-client3355模块的Controller业务逻辑类添加注解 @RefreshScope 实现刷新功能

  3. 以上配置只能让config-server3344(和git直连)可以热更新git上的配置,但是3355还不能生效。需要向3355发送post请求。curl -X POST “http://localhost:3355/actuator/refresh
  4. 但如果存在多个微服务config客户端,每个微服务模块都需要一次post请求和手动刷新,因此想要一种一次通知处处生效的广播行为,但目前还无法实现。

    二、springcloud Bus消息总线

    Bus支持两种信息代理:Kafka和RabbitMQ.Springcloud Bus配合Springcloud config实现配置的动态刷新。Springcloud Bus是用来将分布式系统的节点和轻量级消息系统链接起来的框架,整合了java的事件处理机制和消息中间件的功能。
    image.png
    springcloud Bus能管理和传播分布式系统间的消息,就像一个分布式执行器,可用于广播状态更改、事件推送等,也可以当做微服务间的通信通道。image.png
    � 总线是什么❓ 微服务架构系统中,通常会使用轻量级的消息代理来构建一个共用的消息主题,并让系统中所有微服务实例都连接上。该主题中产生的信息会被所有实例监听和消费,称为消息总线。在总线上的每个实例,都可以方便地广播一些需要让其他连接在该主题上的实例都知道的消息。
    基本原理❓ ConfigClient实例都监听MQ中同一个topic(默认是springcloudBus)。当一个服务刷新数据时,会把信息放入topic中,这样其他监听同一个topic的服务就能得到通知,然后去更新自身配置。

    Spring Cloud Bus动态刷新全局广播

    在config-server模块(3344、3355、3366配置 ```xml
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-bus-amqp</artifactId>
    </dependency>
```yaml
spring:
    rabbitmq:
      host: 47.99.60.60
      username: admin
      password: 123
      port: 5672

向config-server3344模块发送 curl -X POST “http://localhost:3344/actuator/bus-refresh”
更改git上的info信息配置,
如果出错,可能是一下原因:

  1. 各config-client模块的controller方法是否添加 @RefreshScope注解
  2. 各config模块是否添加rabbimq消息总线的依赖和配置,配置需要在spring下,注意缩进
  3. 修改文件后不重启,但是要记得给config-server模块发送post请求,cmd不报错
  4. 3344的配置文件 ```yaml

rabbitmq

management: endpoints: web: exposure: include: “bus-refresh”

                     ![截屏2022-02-11 下午5.31.56.png](https://cdn.nlark.com/yuque/0/2022/png/25505217/1644571926871-b24215e8-6bd2-4069-aded-caa07a5952e7.png#clientId=u6509290f-1f60-4&crop=0&crop=0&crop=1&crop=1&from=drop&id=uabd18ef7&margin=%5Bobject%20Object%5D&name=%E6%88%AA%E5%B1%8F2022-02-11%20%E4%B8%8B%E5%8D%885.31.56.png&originHeight=172&originWidth=772&originalType=binary&ratio=1&rotation=0&showTitle=false&size=44252&status=done&style=none&taskId=u18114990-8ee1-45b7-8f34-5d989dff500&title=)<br />rabbitmq队列增加 springcloudbus总线,类似于公众号,git服务端的一次修改可以广播通知所有接口。
<a name="q1c6p"></a>
#### Spring Cloud Bus动态刷新定点通知
**只通知**3355而不通知3366:<br />**curl -X POST "http://localhost:3344/actuator/bus-refresh/config-client:3355"**
<a name="OgSg2"></a>
## 三、springcloud-stream 消息驱动
是什么❓ 不需要关注具体mq的细节,只需要用一种适配绑定的方式,自动的在mq间切换。<br />屏蔽底层消息中间件的差异,降低切换成本,同喜消息的编程模型。<br />springcloud stream是一个构建消息驱动微服务的框架。应用程序通过Inputs(生产者或outputs(消费者与springcloud stream的**binder对象**交互,通过我们来配置binding。springcloud stream的binder对象负责与消息中间件交互。<br />使用spring integration来连接消息代理中间件以实现消息事件驱动。<br />springcloud stream为一些供应商的消息中间件产品提供了个性化的自动化配置实现,引用了发布-订阅、消费组、分区的三个核心概念。<br />目前仅支持rabbitmq/kafka.<br />设计思想🤔  解耦合。定义绑定器作为中间层,完美实现了应用程序和消息中间件细节之间的隔离
<a name="h5n8z"></a>
### 核心概念
<a name="Pwrr4"></a>
#### Binder
连接中间件,屏蔽差异
<a name="OteAu"></a>
#### Source和Sink
从Stream发布消息是输出,接收消息是输入。
<a name="TOqfM"></a>
#### Channel
通道,队列的一种抽象,在消息通讯系统中是实现存储和转发的媒介,通过Channel对队列进行配置。
```xml
 <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-stream-rabbit</artifactId>
        </dependency>
server:
  port: 8801


spring:
  application:
    name: cloud-stream-provider
  rabbitmq:
    host: 120.27.216.62
    username: admin
    password: 123
    port: 5672
  cloud:
    stream:
      binders:
        defaultRabbit:
          type: rabbit
          environment:
            spring:
              rabbit:
                host: 120.27.216.62
                username: admin
                password: 123
                port: 5672

      bindings:
        input:
          destination: studyExchange  #交换机名称定义
          content-type: application/json  #文本设置为text/plain
          binder: defaultRabbit #要绑定的消息服务的具体设置

eureka:
  client:
    service-url:
      defaultZone: http://eureka7001.com:7001/eureka
  instance:
    lease-renewal-interval-in-seconds: 2 # 心跳间隔时间 ,默认30s
    lease-expiration-duration-in-seconds: 5
    instance-id: send-8801.com #消息列表时显示主机名称
    prefer-ip-address: true #访问路径变为ip地址

消息驱动之消费者 / 生产者

生产者的yml文件

server:
  port: 8802
spring:
  application:
    name: cloud-stream-consumer
  rabbitmq:
    host: 118.31.2.130
    username: admin
    password: 123
    port: 5672
  cloud:
    stream:
      binders:
        defaultRabbit:
          type: rabbit
          environment:
            spring:
              rabbit:
                host: 118.31.2.130
                username: admin
                password: 123
                port: 5672

      bindings:
        input:
          destination: studyExchange  #交换机名称定义
          content-type: application/json  #文本设置为text/plain
          binder: defaultRabbit #要绑定的消息服务的具体设置
eureka:
  client:
    service-url:
      defaultZone: http://eureka7001.com:7001/eureka
  instance:
    lease-renewal-interval-in-seconds: 2 # 心跳间隔时间 ,默认30s
    lease-expiration-duration-in-seconds: 5
    instance-id: receive-8802.com #消息列表时显示主机名称
    prefer-ip-address: true #访问路径变为ip地址

生产者的service方法

@EnableBinding(Source.class)    //定义消息推送管道
public class MessageProviderImpl implements IMessageProvider {

    //消费者
    @Resource
    private MessageChannel output;

    @Override
    public String send() {
        String serial = UUID.randomUUID().toString();
        output.send(MessageBuilder.withPayload(serial).build());
        System.out.println("***Serial:"+serial);
        return null;
    }
}

消费者的controller方法

@RestController
//将信道channel和exchange绑定
@EnableBinding(Sink.class)
public class ReceiveMsgListenerController {
    @Value("${server.port}")
    private String serverPort;

    @StreamListener(Sink.INPUT)
    public void input(Message<String> message)
    {
        //  output.send(MessageBuilder.withPayload(serial).build());
        System.out.println("消费者1号接收到的消息:"+message.getPayload()+"\tport:"+serverPort);
    }

}
  • Middware 中间件,目前只支持RabbitMQ和Kafka
  • Binder 应用和消息中间件的封装,可以动态的改变消息类型,通过配置文件实现
  • @Input 注解标识输入通道,通过该输入通道将接收到的消息进入应用程序
  • @Output 注解表示输出通道 发布的消息通过该通道离开应用程序
  • @StreamListener监听队列 用于消费者队列的消息接收
  • @EnableBinding 信道channel和exchange绑定在一起

    重复消费问题

    在两个消费者中的配置文件添加分组消息,设置同组,避免重复消费
    spring:
      cloud:
        stream:
          bindings:
            input:
              group: atcompanyA ##8802和8803位于相同组
    
    分组不仅可以避免重复消费,若消费者宕机后重启,也可以实现消息的持久化。消费关机状态时mq的消息

四、SpringCloud Sleuth分布式请求链路跟踪

微服务框架中,一个由客户端发起的请求在后端系统中会经过多个不同的服务节点调用来协同产生最后的请求结果,每一个前端请求都会形成一条复杂的分布式服务调用链路,链路中的任何一环出现高延时或错误都会引起整个请求的失败。
SpringCloud从F版不需要自己构件Zipkin Server,直接导入jar包。
java -jar zipkin-server-2.12.9-exec.jar
启动成功后访问 http://localhost:9411/zipkin
一条链路通过trace id唯一标识,span标识发起的请求信息,各span通过parent id。Trace类似于树结构的span集合,标识一条调用链路,存在唯一标识。span表示调用链路来源,span就是一次请求信息。
在 consumer80和provider8001中均配置 模拟链路调用

    <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-zipkin</artifactId>
        </dependency>

配置文件

spring:
  zipkin:
      base-url: http://localhost:9411
    sleuth:
      sampler:
        #采样率 1表示全部采集
        probability: 1

SpringCloud Alibaba

服务限流降级:默认支持Servelt,feign,restTemplate,Dubbo和RocketMQ限流降级功能的接入,可以在运行时通过控制台实时修改限流降级规则,支持查看限流降级Metrics监控
服务注册与发现:适配springcloud服务注册与发现标准,默认集成Ribbon
分布式配置管理:支持分布式系统中的外部化配置,配置更改时自动刷新
消息驱动能力:基于springcloud stream为微服务应用构建消息驱动能力
阿里云对象存储:阿里云提供海量、安全、低成本、高可靠的云存储服务。支持在任何应用、任何时间、任何地点存储和访问任意类型的数据。
分布式任务调度:提供秒级、精准、高可靠、高可用的定时(基于Cron表达式)任务调度服务。同时提供分布式的任务执行模型,如网格任务。网格任务支持海量子任务均匀分配到所有Worker(schedulerx-client)上执行。
[

](https://spring.io/projects/spring-cloud-alibaba)
GitHub:https://github.com/alibaba/spring-cloud-alibaba
GitHub中文文档:https://github.com/alibaba/spring-cloud-alibaba/blob/master/README-zh.md
Spring Cloud Alibaba参考文档:https://spring-cloud-alibaba-group.github.io/github-pages/greenwich/spring-cloud-alibaba.html

SpringCloud Alibaba Nacos 服务注册和配置中心

Naming Configuration Service一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。
替代eureka做服务注册中心,替代config做服务配置中心。
官网:https://nacos.io/zh-cn/
进入nacos的bin目录,sh startup.sh -m standalone
运行成功后访问 http://127.0.0.1:8848/nacos/index.html#/login
账号密码都是 nacos

服务注册中心

父工程模块导入

  <!--spring cloud 阿里巴巴-->
  <dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-alibaba-dependencies</artifactId>
    <version>2.1.0.RELEASE</version>
    <type>pom</type>
    <scope>import</scope>
  </dependency>

子工程模块
nacos集成了spring-cloud-starter-netflix-ribbon ,具有负载均衡的功能.
但是restTemplate组件使用时需要添加注解 @LoadBalanced

<dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
@Configuration
public class ApplicationContextConfig {

    @Bean
    @LoadBalanced
    public RestTemplate restTemplate(){
        return new RestTemplate();
    }
}
                                                                                                                                                                                             虚拟映射克隆模块                    ![截屏2022-02-12 下午7.41.34.png](https://cdn.nlark.com/yuque/0/2022/png/25505217/1644666098463-943270a8-e47f-4c75-9e6e-7e3ab89ab9e5.png#clientId=u798e2700-9a70-4&crop=0&crop=0&crop=1&crop=1&from=drop&id=udedfd962&margin=%5Bobject%20Object%5D&name=%E6%88%AA%E5%B1%8F2022-02-12%20%E4%B8%8B%E5%8D%887.41.34.png&originHeight=760&originWidth=1056&originalType=binary&ratio=1&rotation=0&showTitle=false&size=108258&status=done&style=none&taskId=u19d1241e-b6aa-4ecf-86f1-edd977c48f5&title=)

对比

image.png nacos支持cp和ap的切换image.pngconsistency 是所有节点在同一时间看到的数据一致。Availiable是所有的请求都会收到响应。
一般来说,不需要存储服务级别的信息且服务实例通过nacos-client注册,能保持心跳上报,使用AP模式。如springcloud.dubbo.ap为了服务的可能性而减弱了一致性。ap只支持临时注册。
如果需要服务级别编辑或存储配置信息,使用cp。k8s和dns服务是cp模式,cp模式下支持注册持久化实例,此时以raft协议为集群运行模式,该模式下注册实例必须先注册服务,如果服务不存在,会返回错误。

服务配置中心

<dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
        </dependency>
  nacos配置模块需要 application.yml和 bootstrap.yml两个配置文件。bootstrap的优先级更高。同springcloud-config一样,项目初始化时从配置中心进行配置拉取。<br />_${spring.application.name}-${spring.profile.active}.${spring.cloud.nacos.config.file-extension}_

image.pngimage.png
动态刷新:修改nacos的yaml配置文件,再次调用配置接口,配置已经刷新。

分类配置

多环境多项目的微服务配置管理。
Namespace+group+service。最外层的namespace是用于区分部署环境。group和dataId逻辑上区分两个目标对象。
默认情况: Namesapce=public Group=DEFAULTGROUP Cluster=DEFAULT
namespace用来隔离。group把不同的微服务划分到同一个分组。service就是微服务,一个service包含多个cluster,nacos默认cluster是DEFAULT,cluster是对指定微服务的虚拟划分。可以让同一个机房的微服务相互调用,提升性能。instance是微服务的实例。
${spring.application.name}-${spring.profile.active}.${spring.cloud.nacos.config.file-extension}_

  • dataID配置

配置一个group下不同dataid(开发环境不同spring.profile.active)的多个配置,在application.yml配置文件中配置哪个spring.profiles.active加载哪个。

spring:
  profiles:
    active:
  • Group方案

同一个DataId下不同Group。对于开发环境的加载。

  1. group(bootstrap.yml

    spring:
     cloud:
         nacos:
         config:
           group: TEST_GROUP
        #group: DEV_GROUP
    
  2. spring.profile.active(application.yml

    spring:
     profiles:
       active: info
    

    截屏2022-02-12 下午10.53.37.png

  • Namespace
  1. 新建命名空间(dev/test
  2. 根据命名空间的命名空间id 在配置文件中增加配置

    spring:
     cloud:
       nacos:
         config:
           namespace: 9826a557-8c65-4672-85f6-6453a692344d 
         #该namespace是dev命名空间的id 因此对应 spring.profiles.active=dev
    
  3. 根据命名空间 在命名空间下的配置列表进行配置

会读取dev命名空间下的devgroup的dev的yaml配置文件 截屏2022-02-12 下午10.58.07.png

集群和持久化配置

默认nacos使用嵌入式数据库(derby)实现数据存储,启动多个默认配置下的nacos节点,数据存储存在一致性问题。为了解决这个问题,nacos采用了集中式存储的方式来支持集群化部署,目前只支持mysql的存储。

  • 单机模式
  • 集群模式。确保高可用
  • 多集群模式。用于多数据中心场景

nacos/conf/nacos-mysql.sql 执行文件sql语句脚本
在同文件夹下的application.properties中添加语句

###############################################################
spring.datasource.platform=mysql

db.num=1
db.url.0=jdbc:mysql://127.0.0.1:3306/nacos_config?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true
db.user=root
db.password=88888888

linux中nacos集群配置

  1. conf/cluster.conf 配置ip地址和端口号的映射
  2. 集群启动时需要传递不同的端口号启动不同的nacos实例。./startup.sh -p xx 启动端口号与nacos实例要根据cluster.conf设置的匹配
  3. 进入/usr/local/nginx/conf/nginx.conf
    1. 修改监听端口 server-listener
    2. 修改server-location proxy_pass http://cluster

修改#gzip on的正下方 加入 upstream cluster{集群ip: 端口号}

  1. 进入 /usr/local/nginx/sbin 启动nginx ./nginx -c /usr/local/nginx/conf/nginx.conf
  2. 启动nacos 进入/mynacos/bin ./startup.sh -p 端口号
  3. ps -ef | grep nacos | grep -v grep | wc -l 查看启动的集群个数
  4. ps -ef | grep nginx 查看nginx是否启动
  5. 通过nginx访问nacos节点 http://localhost:conf中的端口号/nacos

    SpringCloud Alibaba Sentinel实现熔断与限流

    随着微服务的流行,服务与服务之间的稳定性越来越重要。Sentinel以流量为切入点,从流量控制,熔断降级,系统负载保护等多个维度维护服务稳定性。
  • 丰富的应用场景。秒杀(突发流量控制在系统容量可以承受的范围)、消息削峰填谷、集群流量控制、实时熔断下游不可用应用。
  • 完备的实施控制
  • 广泛的开源生态
  • 完善的SPI扩展点

官网:https://github.com/alibaba/sentinel
Sentinel分为两个部分

  1. 核心库(java客户端。不依赖任何框架/库,运行于所有java运行时环境,对Dubbo/Springcloud等框架有较好支持。
  2. 控制台(Dashboard).基于sb开发,打包后直接运行,不需要额外的Tomcat等应用。

java -jar sentinel-dashboard-1.8.3\ .jar 运行
运行后打开http://localhost:8080 账号和密码都是sentinel
pom.xml: sentinal和nacos一起配置

    <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>

<!--        持久化-->
        <dependency>
            <groupId>com.alibaba.csp</groupId>
            <artifactId>sentinel-datasource-nacos</artifactId>
        </dependency>
<!--        alibaba sentinel-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
        </dependency>
<!--        openfeign-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
server:
  port: 8401

spring:
  application:
    name: cloudalibaba-sentinel-service
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
    sentinel:
      transport:
        dashboard: localhost:8080
        #假如8080端口被占用 会自动从8719开始扫描依次+1,直到找到空闲端口
        port: 8719

management:
  endpoints:
    web:
      exposure:
        include: "*"

启动后sentinel8080监控service模块8401

流控规则

资源名: 唯一名称,默认请求路径
针对来源:Sentinel可以针对调用者进行限流,填写微服务名,默认default(不区分来源
阈值类型/单机阈值(Blocked by Sentinel (flow limiting)):
QPS(每秒钟请求数量query for second):当调用该api的QPS达到阈值,进行限流
线程数:当调用该api的线程数达到阈值,进行限流
是否集群:不需要集群
流控模式:
直接:api达到限流条件,直接限流
关联:当关联的资源达到阈值,就限流自己
链路:指记录指定链路上的流量(指定资源从入口资源进来的流量,如果达到阈值,就进行限流)。【api级别的针对来源】
流控效果:
快速失败:直接失败,抛异常
warm up: 预热/冷启动方式。系统长期处于低水位,当流量突然增加,直接把系统拉到高水位可能瞬间压垮系统。通过冷启动,让通过的流量缓慢增加,一定时间内逐渐增加到阈值上限,给冷启动预热时间,避免冷系统被冲垮。根据codeFactor(冷加载因子,默认3)的值,从阈值threshold/codeFactor,经过预热时长,才达到设置的QPS阈值。
排队等待:均匀排队,让请求匀速通过,阈值类型必须设置为QPS,否则无效。对应漏桶算法。
截屏2022-02-13 下午7.43.29.png1

降级规则

RT(平均响应时间,秒级)
平均响应时间超出阈值 在时间窗口内通过的请求>=5,两个条件同时满足触发降级。窗口期过后关闭断路器。RT最大4900. -Dcsp.sentinel.statistic.max.rt=XX
异常比例(秒级):
QPS>=5且异常比例(秒级统计)超过阈值时,触发降级,对该方法的调用会自动返回;时间窗口结束后,关闭降级。
异常数(分钟级):
超过阈值,触发降级,进行熔断。时间窗口结束后,关闭降级(时间窗口时长要大于60s。
sentinel没有半开状态。所谓半开状态就是系统自动检测请求是否有异常,没有异常就关闭断路器恢复使用。存在异常就继续打开断路器,不可用。

热点规则

热点是经常访问的数据。统计传入参数中的热点参数,根据配置的限流阈值与模式,对包含热点参数的资源调用进行限流。热点参数是一种特殊的流量控制,仅对包含热点参数的资源调用生效。
截屏2022-02-14 下午4.21.59.png

              //资源名 
    @SentinelResource(value="testHotKey",blockHandler = "deal_hotkey")
    @GetMapping(value="/testHotKey")
    public String testHotKey(@RequestParam(value = "p1",required = false)String p1,
                             @RequestParam(value = "p2",required = false)String p2)
    {

        return "____testhotkey";

    }
    public String deal_hotkey(String p1, String p2, BlockException exception){
        return "处理热点规则";

    }

参数例外项

参数索引项不能超过单机阈值,但是当参数值为所设置后,可以特设一个阈值。

                                                              ![截屏2022-02-14 下午4.43.43.png](https://cdn.nlark.com/yuque/0/2022/png/25505217/1644828228282-e7f22128-f705-44ee-abec-bb28d51d2cdc.png#clientId=ubc588d51-f314-4&crop=0&crop=0&crop=1&crop=1&from=drop&id=u14191141&margin=%5Bobject%20Object%5D&name=%E6%88%AA%E5%B1%8F2022-02-14%20%E4%B8%8B%E5%8D%884.43.43.png&originHeight=675&originWidth=659&originalType=binary&ratio=1&rotation=0&showTitle=true&size=65840&status=done&style=none&taskId=ue169cb42-e2b1-4e0a-9de4-038c717fa4f&title=%E5%BD%93%E7%B4%A2%E5%BC%95%E4%B8%BA0%E7%9A%84%E5%8F%82%E6%95%B0%E5%80%BC%E4%B8%BA5%E6%97%B6%EF%BC%8C%E9%98%88%E5%80%BC%E4%B8%BA200%E8%80%8C%E4%B8%8D%E6%98%AF1 "当索引为0的参数值为5时,阈值为200而不是1")<br />但是当控制器方法出现异常,不会使用blockHandler的方法进行兜底。@SentinelResource处理的是Sentinel控制器配置的违规情况。

系统规则

系统自适应限流。从整体维度对应用入口流量进行控制,结合应用的Load、cpu使用率、总体平均RT、入口QPS和并发线程数等几个维度的监控指标,通过自适应的流控策略,让系统的入口流量和系统的负载达到平衡,尽可能跑在最大吞吐量的同时保证系统整体稳定性。
Load自适应: 仅对linux和unix-like机器生效,系统的load1作为启发指标,进行自适应系统保护。当系统load1超过设定的启发值,且系统当前的并发线程数超过估算的系统容量时才会触发系统保护(BBR).
截屏2022-02-14 下午4.51.48.png

@SetinalResource

资源名称限流

@RestController
public class RateLimitController {
    @GetMapping(value="/byResource")
    @SentinelResource(value="byResource",blockHandler="handleException")
    public CommonResult byResource(){
        return new CommonResult(new Payment(2020L,"serial001"),"按资源名称限流测试",200);
    }
    public CommonResult handleException(BlockException exception){
        return new CommonResult("服务不可用",exception.getClass().getCanonicalName(),444);
    }
}

url地址限流

使用系统默认的方法兜底(Blocked by Sentinel (flow limiting)

 @GetMapping("/rateLimit/byUrl")
    @SentinelResource(value="byUrl")
    public CommonResult byUrl(){
        return new CommonResult(new Payment(2020L,"serial002"),"按uRL名称限流测试",200)
    }

自定义限流处理逻辑

新建handler方法 自定义handler页面


public class CustomerBlockHandler {
    public static CommonResult handlerException(BlockException exception){
        return new CommonResult(new Payment(2020L,"serial003"),"handlerException客户自定义限流测试----1",4444);


    }

    public static CommonResult handlerException2(BlockException exception){
        return new CommonResult(new Payment(2020L,"serial003"),"handlerException客户自定义限流测试----2",4444);

    }
}

控制器方法配置

    @GetMapping(value="/rateLimit/customerBlockHandler")
    @SentinelResource(value="customerBlockHandler",blockHandlerClass = CustomerBlockHandler.class,blockHandler = "handlerException")
    public CommonResult customerBlockHandler(){
        return new CommonResult(new Payment(2020L,"serial002"),"客户自定义限流测试",200);
    }

@SentinelResource 定义资源,提供可选的异常处理和fallback配置项。埋点不支持private方法。·

三个核心api

  • Sphu 定义资源
  • Tracer 定义统计
  • ContextUtil 定义上下文

服务熔断

ribbon+sentinel

fallback可以管理运行时异常。
fallback和blockHandler都进行配置,会被限流降级抛出BlockException进入blockHandler处理逻辑。
exceptionsToIgnore={.class} 该参数定义的异常类不会被处理。

feign+sentinel

feign:
    sentinel:
      enabled: true

feign 接口Service+注解@FeignClient

规则持久化

当sentinel模块重启后,配置的规则都会消失,是临时节点。生产环境需要持久化。可以将限流配置规则持久化进入nacos保存。
持久化依赖

       <dependency>
            <groupId>com.alibaba.csp</groupId>
            <artifactId>sentinel-datasource-nacos</artifactId>
        </dependency>

配置文件修改

sentinel:
    datasource:
      ds1:
        nacos:
          server-addr: localhost:8848
        dataId: cloudalibaba-sentinel-service
        groupId: DEFAULT_GROUP
        data-type: json
        rule-type: flow

nacos中根据dataid groupId新建配置 格式选择json

[
  {
    "resource":"/rateLimit/byUrl",    //资源名称
    "limitApp":"default",                        //来源应用
    "grade":1,                                            //阈值类型 0是并发线程数 1是qps
    "count":1,                                            //单机阈值
    "strategy":0,                                        //流控模式,0-直接,1-关联,2-链路
    "controlBehavior":0,                        //流控效果,0-快速失败,1-warm up,2-排队等待
    "clusterMode":false
  }    
  ]

Springcloud alibaba Seata处理分布式事务

分布式事务:一次业务操作需要跨多个数据源或跨多个系统进行远程调用,就会产生分布式事务问题。主要是要保证全局一致性。
seata,开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务。
Transaction ID XID:全局唯一的事务ID
Transaction Coordinator (TC:事务协调器,维护全局事务的运行状态,负责协调并驱动全局事务的提交或回滚
Transaction Manageer(TM:控制全局事务的边界,负责开启一个全局事务,并最终发起全局提交或全局回滚的决议
Resource Manager(RM:控制分支事务,负责分支注册、状态汇报,并接收事务协调器的指令,驱动分支(本地)事务的提交或回滚。

  1. TM向TC申请开启一个全局事务,全局事务创建成功并生成一个唯一的XID
  2. XID在微服务调用链路的上下文中传播
  3. RM向TC注册分支事务,将其纳入XID对应全局事务的管辖
  4. TM向TC发起针对XID的全局提交或回滚决议
  5. TC调度XID下管辖的全部分支事务完成提交或回滚请求

@GlobalTransactional
Simple Extensible Autonomous Transaction Architecture 简单可扩展自治事务框架。

AT模式

  • 基于支持本地ACID事务的关系型数据库
  • java应用,通过jdbc访问数据库

一阶段加载:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和链接资源
image.png
image.png
二阶段提交:
提交异步化
image.png
二阶段回滚
回滚通过一阶段的回滚日志进行反向补偿
image.png