谷粒商城核心技术栈总结

前言

本文来源于尚硅谷谷粒商城项目,对其中的核心技术栈做了提炼和总结

由于我笔记中用的是电脑的文件路径,所以图片无法显示,请谅解

nginx相关知识

正向代理和反向代理

image-20231121200303598

.

正向代理
  • 在客户端部署代理服务器,代替客户端对外部网络发送和接收消息。客户端发送一个指定目标的请求给代理服务器,代理服务器再发送给目标服务器,目标服务器收到请求后,将响应的内容发送给代理服务器,代理服务器发给客户端

  • 在正向代理的过程中,代理服务器代替客户端向目标服务器发送请求,目标服务器不知道谁是真正的客户端,不知道访问自己的是一个代理服务器还是客户端服务器只负责将响应包发送给请求方

反向代理
  • 服务器端部署代理服务器(为了区分,将真正响应的服务器成为业务服务器),让代理服务器替业务服务器接收请求或发送响应

  • 客户端发送一个请求给代理服务器,代理服务器接收请求并将请求发送给业务服务器,业务服务器将响应发送给代理服务器,代理服务器再将响应发送给客户端

  • 在反向代理的过程中,客户端不知道自己请求的是代理服务器还是业务服务器

配置文件

image-20231121200323764

.

  • nginx.conf中还会包含conf.d目录下的所有配置文件

    image-20231121200630149

!!!异步

线程
初始化线程的4种方式
  • 4种方式

  • 继承 Thread

  • 实现 Runnable 接口

  • 实现Callable接口 + FutureTask (可以拿到返回结果,可以处理异常)

    • get功能:阻塞等待线程完成之后,获取返回结果

      FutureTask<Integer> futureTask = new FutureTask<>(new Callable01());
      new Thread(futureTask).start();
      System.out.println(futureTask.get());
      public static class Callable01 implements Callable<Integer> {
          @Override
          public Integer call() throws Exception {
              System.out.println("当前线程:" + Thread.currentThread().getId());
              int i = 10 / 2;
              System.out.println("运行结果:" + i);
              return i;
          }
      }
  • 线程池 【每个异步任务给线程池执行即可】

    public static ExecutorService executor = Executors.newFixedThreadPool(10);//指定线程池中线程的数量
    ​
    service.execute(new Runable01());//普通执行
    Future<Integer> submit = service.submit(new Callable01());//执行完后可以获取返回值
    submit.get();
    • 或者原生创建方式【详情就看下方线程池的七大参数

    new ThreadPoolExecutor
        (corePoolSize, maximumPoolSize, keepAliveTime, TimeUnit unit, workQueue, threadFactory, handler); 
  • 比较

    • 方式1和方式2:主进程无法获取线程的运算结果,不适合当前场景

    • 方式3:主进程可以获取线程的运算结果,但是不利于控制服务器中的线程资源,可能导致服务器资源耗尽

    • 方式4:通过如下两种方式初始化线程池

      • 如果不使用线程池,一个异步任务就创建一个线程,容易导致资源耗尽,以后的业务中,多线程异步任务都交给线程池执行

      • 线程池性能稳定,也可以获取执行结果,并捕获异常

      • 但是在业务复杂情况下,一 个异步调用可能会依赖于另一个异步调用的执行结果

线程池的七大参数

image-20231201190823280

.

  • corePoolSize: 核心线程数,当最大线程数为maximumPoolSize时,池中一直保持的线程的数量

    • 即使线程空闲,只要线程池不销毁就一直存在,除非设置了allowCoreThreadTimeOut

    • 在创建线程池时,会创建corePoolSize数量的空闲线程【2024.3.7根据JavaGuide的面经进行补充:现在大部分的线程池都是动态创建,如果最大线程数不为maximumPoolSize,那核心线程数就是最大线程数】

  • maximumPoolSize:池中允许的最大的线程数,用于控制资源

  • keepAliveTime:存活时间,作用是释放空闲线程

    • 当线程数大于核心线程数的时候,线程在存活时间到达的时候没有接到新任务就会终止释放

    • 最终线程池维持在corePoolSize大小【把超了核心大小且空闲的线程释放掉】

  • unit:时间单位

  • workQueue:阻塞队列,用来存储等待执行的任务

    • 如果当前对线程的需求超过了corePoolSize大小,就会放在这里等待空闲线程执行

    • 只要有线程空闲,就会去队列中取任务【所以人都忙着,新任务先堆积,有人闲下来就会去做】

    • 如果阻塞队列满了,还要新任务就创建新线程【人手不够了,再堆下去做不完了】

    • 阻塞队列有很多种,其中LinkedBLockingDeque<>()容量默认是Integer的最大值,容易导致内存不够,一定要指定容量

  • threadFactory:创建线程的工厂,比如指定线程名等

  • handler:拒绝策略,如果满了最大线程数,线程池就会使用拒绝策略拒绝执行任务

    • AbortPolicy()//默认丢弃策略,如果最大线程数满了,新任务就丢弃,而且会抛异常

ExecutorService threadPool = new ThreadPoolExecutor(
        200,
        10,
        10L,
        TimeUnit.SECONDS,
        new LinkedBlockingDeque<Runnable>(10000),
        Executors.defaultThreadFactory(),
        new ThreadPoolExecutor.AbortPolicy()//默认丢弃策略,如果最大线程数满了,新任务就丢弃,而且会抛异常
);
线程池工作顺序
  1. 线程池创建,准备好corePoolSize数量的核心线程,准备接受任务

  2. 新的任务进来,先用corePoolSize准备好的空闲线程执行

    1. 核心线程都被占满了,就将再进来的任务放入阻塞队列中。核心线程空闲就会自己去阻塞队列获取任务执行

    2. 阻塞队列满了,就直接开新线程执行,最多只能开到maximumPoolSize指定的数量

    3. 任务都执行好了,(maximumPoolSize-corePoolSize)数量空闲的线程会在 keepAliveTime指定的时间后自动销毁。最终保持到corePoolSize大小

    4. 如果线程数达到maximumPoolSize的数量,还有新任务进来,就会使用reject指定的拒绝策略进行处理

  3. 所有的线程创建都是由指定的factory创建的

面试题
  • 一个线程池 core 7; max 20 ,queue:50,100 并发进来怎么分配的【最坏情况如下

    • 先有7个能直接得到执行,接下来50个进入队列排队

    • 再在多开13个线程继续执行

    • 现在70个任务被安排上了,剩下30个任务使用拒绝策略

常见的4种线程池
  • newCachedThreadPool:创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程

    • corePoolSize=0,空闲线程都会回收

    image-20231201193432107

    .

  • newFixedThreadPool:创建一个定长线程池,可控制线程最大并发数,超出的任务会在队列中等待

    • corePoolSize=maximumPoolSize,空闲线程都不回收

    image-20231201193500789

    .

  • newScheduledThreadPool: 创建一个定时任务线程池,支持定时及周期性任务执行

  • newSingleThreadExecutor:创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行

开发中为什么使用线程池
  • 降低资源的消耗: 通过重复利用已经创建好的线程降低线程的创建和销毁带来的损耗

  • 提高响应速度

    • 线程池中的线程数没有超过线程池的最大上限时,有的线程处于等待分配任务的状态,当任务来时无需创建新的线程就能执行

    • 减少线程等待时间片的时间,因为cpu只用在固定数量的线程中进行切换,而不是按任务数量进行切换

      • 没使用线程池时,假设有1000个任务,cpu就需要在1000个线程中进行切换

      • 使用线程池且指定最大线程数量为200,cpu只需要在200个线程中进行切换

  • 提高线程的可管理性

    • 线程池会根据当前系统特点对池内的线程进行优化处理,减少创建和销毁线程带来的系统开销

    • 无限的创建和销毁线程不仅消耗系统资源,还降低系统的稳定性,使用线程池进行统一分配

CompletableFuture异步编排
业务场景
  • 查询商品详情页的逻辑比较复杂,有些数据还需要远程调用,必然需要花费更多的时间

  • 假如商品详情页的每个查询,需要如下标注的时间才能完成,用户需要5.5s 后才能看到商品详情页的内容

    • 如果有多个线程同时完成这 6 步操作,也许只需要1.5s即可完成响应

    • 而且查询4、5、6需要1的结果,所以需要异步编排

image-20231201194849080

.

Future和CompletableFuture
  • Future用来描述一个异步计算的结果,提供了异步执行任务的能力

    • isDone方法检查计算是否完成,或者使用get阻塞住调用线程,直到计算完成返回结果

    • cancel 方法停止任务的执行

    • Future对于结果的获取很不方便

      • 只能通过阻塞或者轮询的方式得到任务的结果

      • 阻塞的方式显然和我们的异步编程的初衷相违背

      • 轮询的方式又会耗费无谓的 CPU 资源,而且也不能及时地得到计算结果

  • Java 8新增加了CompletableFuture,提供了非常强大的Future的扩展功能

    • CompletableFuture 和FutureTask 同属于 Future 接口的实现类,都可以获取线程的执行结果

      image-20231201200220771

      .

    • 可以帮助我们简化异步编程的复杂性,提供了函数式编程的能力,可以通过回调的方式处理计算结果,并且提供了转换和组合 CompletableFuture的方法

    • CompletableFuture 类实现了 Future 接口,所以可以通过get方法阻塞或者轮询的方式获得结果,但是这种方式不推荐使用

创建异步对象
方法
  • CompletableFuture 提供了四个静态方法来创建一个异步操作

    • runXxxx 都是没有返回结果的,supplyXxx都是可以获取返回结果的【可以获得future对象进行异步编排】

    • 可以传入自定义的线程池,否则就用默认的线程池

image-20231202134230103

.

测试
public static ExecutorService executor= Executors.newFixedThreadPool(10);
public static void main(String[] args) {
        System.out.println("方法开始");
//        CompletableFuture.runAsync(()->{
//            System.out.println("当前线程"+Thread.currentThread().getId());
//            System.out.println("运行结果"+10/2);
//        },executor);
        CompletableFuture<Integer> future = CompletableFuture
            .supplyAsync(() -> {
            int i = 10 / 2;
            System.out.println("当前线程"+Thread.currentThread().getId());
            System.out.println("运行结果" + i);
            return i;
        }, executor);
        try {
            Integer integer = future.get();
            System.out.println("方法结束"+integer);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
}

image-20231202134755081

计算完成时回调方法
方法

image-20231202135515926

.

  • whenComplete 可以处理正常和异常的计算结果,exceptionally 处理异常情况

  • whenComplete 和 whenCompleteAsync 的区别

    • whenComplete:是执行当前任务的线程执行继续执行whenComplete的任务

    • whenCompleteAsync:是执行把 whenCompleteAsync这个任务继续提交给线程池来进行执行【重新派发】

    • 方法不以 Async 结尾,意味着 Action使用相同的线程执行,而 Async可能会使用其他线程执行(如果是使用相同的线程池,也可能会被同一个线程选中执行)

测试
  • whenComplete会接收上一次任务执行的结果和异常信息

    • 如果上一次任务有异常,结果就为null

    • 顺序完成,异常就为null

System.out.println("方法开始");
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
    int i = 10 / 2;
    System.out.println("当前线程"+Thread.currentThread().getId());
    System.out.println("运行结果" + i);
    return i;
}, executor).whenComplete((result,exception)->{
    System.out.println("异步任务成功,结果是"+result+",异常是"+exception);
});
System.out.println("方法结束");

image-20231202140158137

image-20231202140248063

  • exceptionally 可以对异常进行处理

    • whenComplete虽然能得到异常信息,但是没法修改返回数据

    System.out.println("方法开始");
    CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
        int i = 10 / 0;
        System.out.println("当前线程" + Thread.currentThread().getId());
        System.out.println("运行结果" + i);
        return i;
    }, executor).whenComplete((result, exception) -> {
        System.out.println("异步任务成功,结果是" + result + ",异常是" + exception);
    }).exceptionally(throwable ->{
        //可以感知异常,同时返回默认值
        return 10;
    });
    try {
        Integer integer = future.get();
        System.out.println("方法结束,返回结果"+integer);
    } catch (InterruptedException e) {
        e.printStackTrace();
    } catch (ExecutionException e) {
        e.printStackTrace();
    }

    image-20231202140816333

    .

handle方法
方法
  • 和complete一样,可对结果做最后的处理(可处理异常),可改变返回值

image-20231202141039164

.

测试
System.out.println("方法开始");
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
    int i = 10 / 0;
    System.out.println("当前线程" + Thread.currentThread().getId());
    System.out.println("运行结果" + i);
    return i;
}, executor).handle((result,exception)->{
    if(result!=null){
        return result*2;
    }
    if(exception!=null){
        return Integer.MIN_VALUE;
    }
    return 0;
});
try {
    Integer integer = future.get();
    System.out.println("方法结束,返回结果"+integer);
} catch (InterruptedException e) {
    e.printStackTrace();
} catch (ExecutionException e) {
    e.printStackTrace();
}

image-20231202141342972

image-20231202141357398

线程串行化方法
方法

image-20231202141501482

.

  • thenApply 方法:当一个线程依赖另一个线程时,获取上一个任务返回的结果,并返回当前任务的返回值

  • thenAccept 方法:消费处理结果【消费者只能消费,不能处理】。接收上一个任务的处理结果,并消费处理,无返回结果

  • thenRun 方法:只要上面的任务执行完成,就开始执行thenRun,不会接收上一次的执行结果,而且无返回结果

  • 和之前一样,带有Async默认是去线程池重新找线程,不带就是原本的线程继续执行,以上都要前置任务成功完成

  • Function<? super T,? extends U>

    • T:上一个任务返回结果的类型

    • U:当前任务的返回值类型

测试
  • thenRun 无法感知上一次的执行结果,且无法返回

System.out.println("方法开始");
CompletableFuture<Integer> future1 = CompletableFuture.supplyAsync(() -> {
    int i = 10 / 4;
    System.out.println("当前线程" + Thread.currentThread().getId());
    System.out.println("运行结果" + i);
    return i;
}, executor);
CompletableFuture.supplyAsync(() -> {
    int i = 10 / 4;
    System.out.println("当前线程" + Thread.currentThread().getId());
    System.out.println("运行结果" + i);
    return i;
}, executor).thenRunAsync(()->{
    System.out.println("任务二启动了");
},executor);
  • thenAccept 可以感知上一次的结果,但是无法返回

CompletableFuture.supplyAsync(() -> {
    int i = 10 / 4;
    System.out.println("当前线程" + Thread.currentThread().getId());
    System.out.println("运行结果" + i);
    return i;
}, executor).thenAcceptAsync(result->{
    System.out.println("任务2启动了");
    System.out.println("任务1的结果为"+result);
},executor);
  • thenApply可以感知上一次的结果,并且可以返回当前任务的结果

CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
    int i = 10 / 4;
    System.out.println("当前线程" + Thread.currentThread().getId());
    System.out.println("运行结果" + i);
    return i;
}, executor).thenApplyAsync(result -> {
    System.out.println("任务2启动了");
    System.out.println("任务1的结果为" + result);
    return 666;
}, executor);
System.out.println("任务2的结果"+future.get());

image-20231202142403261

image-20231202142800443

image-20231202143045731

两任务组合-都要完成
方法

image-20231202143401009

image-20231202143411686

image-20231202143427767

  • 组合的两个任务必须都完成才触发该任务

  • thenCombine:组合两个 future,获取两个 future 的返回结果,并返回当前任务的返回值

  • thenAcceptBoth:组合两个 future,获取两个 future 任务的返回结果,然后处理任务,没有 返回值

  • runAfterBoth:组合两个 future,不需要获取 future 的结果,只需两个 future 处理完任务后, 处理该任务

测试
  • runAfterBoth无法感知

CompletableFuture<Integer> future01 = CompletableFuture.supplyAsync(() -> {
    int i = 10 / 4;
    System.out.println("任务1线程" + Thread.currentThread().getId());
    System.out.println("任务1结束" + i);
    return i;
}, executor);
CompletableFuture<String> future02 = CompletableFuture.supplyAsync(() -> {
    int i = 10 / 4;
    System.out.println("任务2线程" + Thread.currentThread().getId());
    System.out.println("任务2结束" + i);
    return "hello";
}, executor);
future01.runAfterBothAsync(future02,()->{
    System.out.println("任务3开始");
},executor);
  • thenAcceptBoth可以感知

future01.thenAcceptBothAsync(future02,(f1,f2)->{
    System.out.println("任务3开始,之前的结果"+f1+"-->"+f2);
},executor);
  • thenCombine可以感知,可以返回

CompletableFuture<Integer> future03 = future01.thenCombineAsync(future02, (f1, f2) -> {
    System.out.println("任务3开始,之前的结果" + f1 + "-->" + f2);
    return 6666;
}, executor);
System.out.println(future03.get());

image-20231202144507606

image-20231202144423499

image-20231202144729297

两任务组合-一个完成
方法

image-20231202145211175

image-20231202145222535

  • 当两个任务中,任意一个 future 任务完成的时候,执行该任务

  • applyToEither:两个任务有一个执行完成,获取它的返回值,处理任务并有新的返回值

  • acceptEither:两个任务有一个执行完成,获取它的返回值【要求两个任务的返回值类型相同】,处理任务,没有新的返回值

  • runAfterEither:两个任务有一个执行完成,不需要获取 future 的结果,处理任务,也没有返回值

测试
  • runAfterEither

CompletableFuture<Integer> future01 = CompletableFuture.supplyAsync(() -> {
    int i = 10 / 4;
    System.out.println("任务1线程" + Thread.currentThread().getId());
    System.out.println("任务1结束" + i);
    return i;
}, executor);
CompletableFuture<String> future02 = CompletableFuture.supplyAsync(() -> {
    int i = 10 / 4;
    System.out.println("任务2线程" + Thread.currentThread().getId());
    try {
        Thread.sleep(3000);
        System.out.println("任务2结束" + i);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    return "hello";
}, executor);
future01.runAfterEitherAsync(future02,()->{
    System.out.println("任务三完成");
},executor);
  • acceptEither接收先完成的任务的返回值,要求组合的任务返回值类型一致

CompletableFuture<Integer> future01 = CompletableFuture.supplyAsync(() -> {
    int i = 10 / 4;
    System.out.println("任务1线程" + Thread.currentThread().getId());
    System.out.println("任务1结束" + i);
    return i;
}, executor);
CompletableFuture<Integer> future02 = CompletableFuture.supplyAsync(() -> {
    System.out.println("任务2线程" + Thread.currentThread().getId());
    try {
        Thread.sleep(3000);
        System.out.println("任务2结束");
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    return 666;
}, executor);
future01.acceptEitherAsync(future02,(res)->{
    System.out.println("任务三开始之前的结果是"+res);
},executor);
  • applyToEither接收先完成任务的返回值,而且返回当前任务的结果,而且组合任务调用get()不会被未完成的那个任务阻塞

CompletableFuture<String> future = future01.applyToEitherAsync(future02, (res) -> {
    System.out.println("任务三开始之前的结果是" + res);
    return "hello";
}, executor);
System.out.println(future.get());

image-20231202152311023

image-20231202152616613

image-20231202152906094

多任务组合
方法

image-20231202153106243

.

  • allOf:等待所有任务完成

  • anyOf:只要有一个任务完成

测试
  • allOf没有返回值

    • 调用get方法要阻塞等待所有任务执行完成

    • 不调用就直接执行

        System.out.println("main start");
        CompletableFuture<String> futureImg = CompletableFuture.supplyAsync(() -> {
            System.out.println("查询商品图片信息");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("查到图片");
            return "hello.jpg";
        }, executor);
        CompletableFuture<String> futureAttr = CompletableFuture.supplyAsync(() -> {
            System.out.println("查询商品属性");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("查到属性");
            return "黑色+256g";
        }, executor);
        CompletableFuture<String> futureBrand = CompletableFuture.supplyAsync(() -> {
            System.out.println("查询品牌信息");
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("查到品牌");
            return "华为";
        }, executor);
        CompletableFuture<Void> allOf = CompletableFuture.allOf(futureImg, futureAttr, futureBrand);
//        allOf.get();//等待所有结果完成
        System.out.println("main end");
  • anyOf有返回值,返回第一个执行完成的结果

CompletableFuture<Object> anyOf = CompletableFuture.anyOf(futureImg, futureAttr, futureBrand);
Object o = anyOf.get();//返回第一个完成的结果
System.out.println(o);
System.out.println("main end"+futureImg.get()+futureAttr.get()+futureBrand.get());

image-20231202154558849

image-20231202155122079

spring session

原理
  • @EnableRedisHttpSession导入RedisHttpSessionConfiguration配置

  • RedisHttpSessionConfiguration给容器中添加的核心组件

    • SessionRepository==》RedisOperationsSessionRepository:Redis操作session,session的增删改查封装类

    • SessionRepositoryFilter==》Filter:session存储过滤器,每个请求过来都必须经过filter

      1. Filter创建的时候,就自动从容器中获取到了SessionRepository

        image-20231209165910998

        .

      2. 核心doFilterInternal方法

        • 先将SessionRepository存入当前请求对象

        • 然后采用装饰模式,先包装了原始请求和响应对象还有上下文环境,然后又包装了原始响应对象

        • 最后将包装后的对象应用到了整个执行链

        image-20231209164915366

        .

      3. 以后获取session实际调用的包装后的wrappedRequest.getSession()==》session是通过SessionRepository获取到的

分布式session共享问题
session原理
  • 访问哪个服务器,哪个服务器就会保存一个jsessionId作为用户标识,之后访问都会携带该jsessionId

image-20231208173625004

.

session共享问题
  • 不能跨不同域名共享访问某个服务,jsessionId只会存储在该服务,而其它服务无法共享jsessionId

  • 同一种服务的jsessionId也是不共享的

image-20231208174000078

.

同一种服务共享解决
session复制

image-20231208175001234

.

  • 优点

    • web-server(Tomcat)原生支持,只需要修改配置文件

  • 缺点

    • session同步需要数据传输,占用大量网络带宽降低了服务器群的业务处理能力

    • 任意一台web-server保存的数据都是所有web- server的session总和,受到内存限制无法水平扩展更多的web-server

    • 大型分布式集群情况下,所有web-server都全量保存数据

  • 此方案不可取

客户端存储

image-20231208174058103

.

  • 优点

    • 服务器不需存储session,用户保存自己的 session信息到cookie中

    • 节省服务端资源

  • 缺点

    • 都是缺点,这只是一种思路

    • 每次http请求,用户都要携带cookie中的完整信息浪费网络带宽

    • session数据放在cookie中,cookie有4K长度限制 ,不能保存大量信息

    • session数据放在cookie中,存在泄漏、篡改、 窃取等安全隐患

  • 这种方式不会使用

hash一致性

image-20231208175535636

image-20231208175544974

  • 优点

    • 只需要改nginx配置,不需要修改应用代码

    • 负载均衡,只要hash属性的值分布是均匀的,多台 web-server的负载是均衡的

    • 可以支持web-server水平扩展(session同步法是不行的,受内存限制)

  • 缺点

    • session还是存在web-server中的,所以web-server重启可能导致部分session丢失,影响业务,如部分用户需要重新登录

    • 如果web-server水平扩展,rehash后session重新分布, 也会有一部分用户路由不到正确的session

    • 但是以上缺点问题也不是很大,因为session本来都是有有效期的

  • 可以使用

统一存储

image-20231208175921394

.

  • 优点

    • 没有安全隐患

    • 可以水平扩展,数据库/缓存水平切分即可

    • web-server重启或者扩容都不会有 session丢失

  • 不足

    • 增加了一次网络调用,并且需要修改应用代码,如将所有的getSession方法替换为从Redis查数据的方式

    • redis获取数据比内存慢很多

  • 上面缺点可以用SpringSession完美解决

不同服务的子域session共享解决
  • 放大域名作用域【前提是不同服务需要部署到同一个父域名】

    • jsessionid这个cookie默认是当前系统域名的。当我们分拆服务,部署到不同域名的时候,可以使用如下解决方案

    • 即一开始获取到session中的jsessionid是当前系统域名,之后需要将jsessionid指定域名为父域名

image-20231208193302200

.

!!!消息队列

消息队列基础概念
应用场景
异步处理
  • 同步模式

image-20231213162145275

.

  • 异步处理:发送邮件和短信可以使用两个异步任务来完成

image-20231213162232522

.

  • 写入消息队列:将发短信和邮件的操作交给消息队列完成,用户无需关注发送时间

image-20231213162336950

.

应用解耦
  • 传统做法:库存系统升级可能导致订单系统需要修改源代码且重新部署

image-20231213163205285

.

  • 消息队列

    • 订单系统写入消息,库存系统不关心接口,通过分析消息的组成来订阅

    • 订单系统无需关心库存系统需要调用什么接口,只需要写消息即可

image-20231213163236422

.

流量控制
  • 大并发请求先写入消息队列,然后写入成功就返回给各个用户

  • 然后后台根据实际业务处理能力对消息进行消费

image-20231213164259478

.

概述
简介
  • 大多应用中,可通过消息服务中间件来提升系统异步通信、扩展解耦能力

  • 消息服务中两个重要概念:消息代理(message broker)和目的地(destination)

    • 当消息发送者发送消息以后,将由消息代理接管,消息代理保证消息传递到指定目的地

    • 消息代理可以理解成安装了消息中间件的服务器

  • 市面的MQ产品:ActiveMQ、RabbitMQ、RocketMQ、Kafka

两种形式的目的地
  • 队列(queue)【点对点式】:点对点消息通信(point-to-point)

    • 消息发送者发送消息,消息代理将其放入一个队列中,消息接收者从队列中获取消息内容消息读取后被移出队列

    • 消息只有唯一的发送者和接受者,但并不是说只能有一个接收者,可以有很多人来访问队列,但每条消息只会给一个人抢到

  • 主题(topic)【发布订阅式】:发布(publish)/订阅(subscribe)消息通信

    • 发送者(发布者)发送消息到主题,多个接收者(订阅者)监听(订阅)这个主题,那么就会在消息到达时同时收到消息

规范以及比较
  • JMS(Java Message Service)JAVA消息服务:基于JVM消息代理的规范,ActiveMQ、HornetMQ是JMS实现

  • AMQP(Advanced Message Queuing Protocol)

    • 高级消息队列协议,也是一个消息代理的规范,兼容JMS

    • RabbitMQ是AMQP的实现

JMS(Java Message Service)AMQP(Advanced Message Queuing Protocol)
定义Java api网络线级协议
跨语言
跨平台
Model提供两种消息模型:Peer-2-Peer、Pub/sub提供了五种消息模型:direct exchange、fanout exchange、topic change、headers exchange、system exchange 本质来讲,后四种和JMS的pub/sub模型没有太大差别 仅是在路由机制上做了更详细的划分
支持消息类型多种消息类型: TextMessage、 MapMessage、 BytesMessage 、StreamMessage 、ObjectMessage、Message (只有消息头和属性)byte[]:当实际应用时,有复杂的消息,可以将消息序列化后发送
综合评价JMS 定义了JAVA API层面的标准;在java体系中,多个client均可以通过JMS进行交互,不需要应用修改代码,但是其对跨平台的支持较差AMQP定义了wire-level层的协议标准;天然具有跨平 台、跨语言特性
Spring和SpringBoot支持
  • spring-jms提供了对JMS的支持

  • spring-rabbit提供了对AMQP的支持

  • 需要ConnectionFactory的实现来连接消息代理

  • 提供JmsTemplate、RabbitTemplate来发送消息

  • @JmsListener(JMS)、@RabbitListener(AMQP)注解在方法上监听消息代理发布的消息

  • @EnableJms、@EnableRabbit开启支持

  • Spring Boot自动配置

    • JmsAutoConfiguration

    • RabbitAutoConfiguration

RabbitMQ概念
简介
  • RabbitMQ是一个由erlang开发的AMQP(Advanved Message Queue Protocol)的开源实现

image-20231213173230662

.

核心概念
Message
  • 消息,消息是不具名的,它由消息头和消息体组成

    • 消息体是不透明的

    • 消息头则由一系列的可选属性组成, 这些属性包括routing-key(路由键)、priority(相对于其他消息的优先权)、delivery-mode(指出该消息可能需要持久性存储)等

Publisher
  • 消息的生产者,也是一个向交换器发布消息的客户端应用程序

Exchange
  • 交换器,用来接收生产者发送的消息并将这些消息路由给服务器中的队列

  • Exchange有4种类型:direct(默认),fanout, topic, 和headers,不同类型的Exchange转发消息的策略有所区别

Queue
  • 消息队列,用来保存消息直到发送给消费者

    • 它是消息的容器,也是消息的终点

    • 一个消息可投入一个或多个队列

    • 消息一直在队列里面,等待消费者连接到这个队列将其取走

Binding
  • 绑定,用于消息队列和交换器之间的关联

  • 一个绑定就是基于路由键将交换器和消息队列连接起来的路由规则,所以可以将交换器理解成一个由绑定构成的路由表

  • Exchange和Queue的绑定可以是多对多的关系

Connection
  • 网络连接,比如一个TCP连接

  • 一个客户端只会建立一条连接来收发数据

  • 消费者连接中断,mq会实时感知消费者下线并将消息存储起来

Channel
  • 信道,多路复用连接中的一条独立双向数据流通道

  • 信道是建立在真实的TCP连接内的虚拟连接,AMQP 命令都是通过信道发出去的,不管是发布消息、订阅队列还是接收消息,这些动作都是通过信道完成【就是在一条连接中开辟多条道路】

  • 因为对于操作系统来说建立和销毁TCP都是非常昂贵的开销,所以引入了信道的概念,以复用一条TCP连接

Consumer
  • 消息的消费者,表示一个从消息队列中取得消息的客户端应用程序

Virtual Hot
  • 虚拟主机,表示一批交换器、消息队列和相关对象,虚拟主机之间是隔离的

  • 虚拟主机是共享相同的身份认证和加密环境的独立服务器域

  • 每个 vhost 本质上就是一个 mini 版的RabbitMQ服务器,拥有自己的队列、交换器、绑定和权限机制

  • vhost是AMQP概念的基础,必须在连接时指定,RabbitMQ 默认的 vhost 是 /

Broker
  • 表示消息队列服务器实体

image-20231213173016173

.

RabbitMQ运行机制
  • AMQP 中的消息路由

    • AMQP 中消息的路由过程和 Java 开发者熟悉的 JMS 存在一些差别,AMQP 中增加了 Exchange 和Binding 的角色

    • 生产者把消息发布到 Exchange 上,消息最终到达队列并被消费者接收

    • 而 Binding 决定交换器的消息应该发送到那个队列

    image-20231214140535879

    .

Exchange类型
分类
  • Exchange分发消息时根据类型的不同分发策略有区别,目前共四种类型:direct、fanout、topic、headers

  • headers匹配 AMQP 消息的 header 而不是路由键,headers 交换器和 direct 交换器完全一致,但性能差很多,目前几乎用不到

direct【完全匹配】

image-20231214141606899

.

  • 消息中的路由键(routing key)如果和Binding 中的 binding key 一致, 交换器就将消息发到对应的队列中

  • 路由键与队列名完全匹配,如果一个队列绑定到交换机要求路由键为“dog”,则只转发 routingkey 标记为“dog”的消息,不会转发 “dog.puppy”,也不会转发“dog.guard” 等等

  • 它是完全匹配、单播的模式

fanout【广播模式】

image-20231214141715041

.

  • 每个发到 fanout 类型交换器的消息都会分到所有绑定的队列上去

  • fanout 交换器不处理路由键,只是简单的将队列绑定到交换器上

  • 每个发送到交换器的消息都会被转发到与该交换器绑定的所有队列

  • 很像子网广播,每台子网内的主机都获得了一份复制的消息

  • fanout类型转发消息是最快的

topic【模糊匹配】

image-20231214142001061

.

  • topic交换器通过模式匹配分配消息的路由键属性,将路由键和某个模式进行匹配,此时队列需要绑定到一个模式上

  • 它将路由键和绑定键的字符串切分成单词,这些单词之间用点隔开

  • 它同样也会识别两个通配符:符号#和符号*#匹配0个或多个单词,*匹配一个单词

安装RabbitMQ
docker安装脚本
docker run -d --name rabbitmq \
-p 5671:5671 -p 5672:5672 -p 4369:4369 -p  25672:25672 -p 15671:15671 -p 15672:15672 \
rabbitmq:management

image-20231213174417889

image-20231213174600455

端口解释
  • 官网:Networking and RabbitMQ | RabbitMQ

  • 4369, 25672 (Erlang发现&集群端口)

  • 5672, 5671 (AMQP端口)

  • 15672 (web管理后台端口)

  • 61613, 61614 (STOMP协议端口)

  • 1883, 8883 (MQTT协议端口)

测试
  • 访问15672端口测试是否启动成功,默认账密都是guest

    image-20231213174703771

    image-20231213174803217

    .

  • 创建交换机

image-20231214143026501

.

  • 创建队列

    image-20231214143409512

    .

  • 交换机绑定【交换机可以绑定队列,也可以绑定交换机(多层路由)】

    image-20231214143635724

    image-20231214143647939

测试不同类型交换机

image-20231214151156219

.

direct
  1. 创建队列

image-20231214151242633

.

  1. 创建direct类型的交换机并绑定队列

    image-20231214151526242

    .

  2. 测试给direact交换机发布消息

    image-20231214151810167

    image-20231214151901448

  3. 接受消息:可以指定接收消息的模式

    • Nack message requeue true接收消息但是将该消息重新放进队列

    • Automatic ack接收消息

    image-20231214152112775

    .

fanout
  1. 创建fanout类型的交换机

image-20231214152804291

.

  1. 添加消息队列

    image-20231214153001028

    .

  2. 发布消息【不需要指定路由键】,即使指定了路由键,但是所有绑定的队列都会收到

    image-20231214153054853

    .

topic
  1. 创建topic类型的交换机

image-20231214153342554

.

  1. 分别绑定以liao开头和以news结尾的所有队列

    image-20231214153549629

    image-20231214153715377

  2. 路由键指定liao.news,发送消息【全都收到】

    image-20231214153840297

    .

  3. 发送给hello.news的路由键

    image-20231214154012919

    .

整合RabbitMQ
环境搭建
  1. 在订单模块中引入spring-boot-starter-amqp

    • 引入启动场景,RabbitAutoConfiguration就会自动生效

    • 自动配置类

      • 给容器中自动配置了CachingConnectionFactory、 RabbitMessagingTemplate、RabbitTemplate、 AmgpAdmin

      • 所有的配置属性绑定在@ConfigurationProperties(prefix = "spring.rabbitmq")

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-amqp</artifactId>
    </dependency>
  2. 配置连接信息

    spring.rabbitmq.host=192.168.32.100
    spring.rabbitmq.port=5672
    spring.rabbitmq.virtual-host=/
  3. @EnableRabbit开启消息队列的监听功能

创建交换机、队列和绑定
  • AmgpAdmin可以创建交换机、队列和绑定

  • 创建direct交换机

DirectExchange directExchange = new DirectExchange("hello-java-exchange", true, false);
amqpAdmin.declareExchange(directExchange);

image-20231214163912245

.

  • 创建队列

    image-20231214164722099

    • exclusive指定该队列是否排他,即该队列只允许有一条连接

Queue queue = new Queue("hello-java-queue",true,false,false);
amqpAdmin.declareQueue(queue);

image-20231214165102288

.

  • 创建绑定需要指定目的地、目的地类型【交换机/队列】、交换机、路由键、自定义参数

    • 将交换机和指定的目的地进行绑定

Binding binding = new Binding("hello-java-queue",Binding.DestinationType.QUEUE,
        "hello-java-exchange","hello.java",null);
amqpAdmin.declareBinding(binding);

image-20231214170317328

.

发送消息
测试发送消息
  • RabbitTemplate可以发送消息

  • 需要指定发送给的交换机、路由键和消息内容

    rabbitTemplate.convertAndSend("hello-java-exchange","hello.java","测试发送消息");

    image-20231216124516926

    image-20231216124849427

  • 测试发送对象【默认采用java的序列化方式,要求发送的对象需要实现序列化接口】

    OrderReturnReasonEntity reasonEntity = new OrderReturnReasonEntity();
    reasonEntity.setId(1l);
    reasonEntity.setCreateTime(new Date());
    reasonEntity.setName("sdad");
    rabbitTemplate.convertAndSend("hello-java-exchange","hello.java",reasonEntity);

    image-20231216124916308

    .

自定义发送消息的序列化机制
@Configuration
public class MyRabbitConfig {
    @Bean
    public MessageConverter messageConverter(){
        return new Jackson2JsonMessageConverter();
    }
}

image-20231216125512139

.

接收/监听消息
接收消息的类型
  • 如果要监听消息就必须标识@EnableRabbit才可以开启监听功能,不需要监听的功能可以不标注

  • @RabbitListener【可以标注在类、方法上】标注在业务逻辑的组件上,并且该组件必须要在容器中,才可以监听消息

    • 属性queues:声明需要监听的所有队列,数组类型

    • 监听消息的方法需要接收消息内容,消息类型可以指定

      • Message:原生消息类型,可以获取消息头和消息体

      • T:发送的消息类型,就不需要收到转换了

      • Channel :当前传输数据的通道

    @RabbitListener(queues = {"hello-java-queue"})
    public void recieveMessage(Object message){
        System.out.println("监听到了"+message);
    }
  • 测试

image-20231216131123259

.

  • 测试接收原生消息类型

    @RabbitListener(queues = {"hello-java-queue"})
    public void recieveMessage(Message message){
        System.out.println("消息体"+message.getBody());
        System.out.println("消息头"+message.getMessageProperties());
        System.out.println("监听到了"+message);
    }

image-20231216131945003

.

  • 测试自动转换成实体类型

    @RabbitListener(queues = {"hello-java-queue"})
    public void recieveMessage(OrderReturnReasonEntity message){
        System.out.println("监听到了"+message);
    }

    image-20231216132121726

    .

多人监听消息
  • 多人都可以监听同一个队列的消息,只要有一人接收到就删除该消息【每条消息只有一个接收者】

  • 测试发送多条消息

    OrderReturnReasonEntity reasonEntity = new OrderReturnReasonEntity();
    reasonEntity.setCreateTime(new Date());
    reasonEntity.setName("sdad");
    for (long i = 0; i < 10; i++) {
        //模拟队列中有多条消息
        reasonEntity.setId(i);
        rabbitTemplate.convertAndSend("hello-java-exchange","hello.java",reasonEntity);
    }
  • 多个接收者同时监听

    image-20231216133031477

    • 同一个消息只能有一个人接收

    image-20231216133042864

    image-20231216133056580

    • 问题:总共发送了10条消息,客户端总共接收到了8条,原因是单元测试会启动一个客户端,参与接收消息

    image-20231216133416483

    .

  • 消费者只有处理完当前消息,才能再去接收消息

@RabbitListener和@RabbitHander
  • @RabbitListener可以标识在类或者方法上【监听哪些队列】,@RabbitHander只能标识在方法上 【重载区分不同的消息】

  • @RabbitHander可以实现接收方法重载,能够将不同类型的消息定位到不同方法

  • @RabbitListener标识在类上接收hello-java-queue队列的所有消息,使用@RabbitHandler接收处理不同的消息类型

@RabbitListener(queues = {"hello-java-queue"})
@Service("orderItemService")
public class OrderItemServiceImpl extends ServiceImpl<OrderItemDao, OrderItemEntity> implements OrderItemService {
    @RabbitHandler
    public void recieveMessage(OrderReturnReasonEntity message) {
        System.out.println("监听到了OrderReturnReasonEntity类型的消息" + message);
    }
    @RabbitHandler
    public void recieveMessage2(OrderEntity message) {
        System.out.println("监听到了OrderEntity类型的消息" + message);
    }
}
  • 发送不同类型的消息

OrderReturnReasonEntity reasonEntity = new OrderReturnReasonEntity();
reasonEntity.setCreateTime(new Date());
reasonEntity.setName("sdad");
OrderEntity orderEntity=new OrderEntity();
orderEntity.setCreateTime(new Date());
for (long i = 0; i < 10; i++) {
    //模拟队列中有多条消息
    if(i%2==0){
        reasonEntity.setId(i);
        rabbitTemplate.convertAndSend("hello-java-exchange","hello.java",reasonEntity);
    }else {
        orderEntity.setId(i);
        rabbitTemplate.convertAndSend("hello-java-exchange","hello.java",orderEntity);
    }
}

image-20231216140350481

.

RabbitMQ消息确认机制-可靠抵达
概念
  • 如果要保证消息不丢失,可靠抵达,可以使用事务消息,但是性能会下降250倍【官方说的】,为此引入确认机制

  • publisher

    • confirmCallback 确认模式,确认消失是否抵达服务器

    • returnCallback 未投递到queue退回模式

  • consumer ack机制

image-20231216141226569

.

ConfirmCallback
  • 消息只要被 broker 接收到就会执行 confirmCallback

    • 如果是 cluster 模式,需要所有broker接收到才会调用confirmCallback

  • 被broker接收到只能表示message已经到达服务器,并不能保证消息一定会被投递到目标queue里

    • 所以需要用到接下来的returnCallback

  • 开启ConfirmCallback功能:新版是spring.rabbitmq.publisher-confirm-type

    • NONE值是禁用发布确认模式,是默认值

    • CORRELATED值是发布消息成功到交换器后会触发回调方法

    • SIMPLE值经测试有两种效果

      • 其一效果和CORRELATED值一样会触发回调方法

      • 其二在发布消息成功后使用rabbitTemplate调用waitForConfirms或waitForConfirmsOrDie方法等待broker节点返回发送结果,根据返回结果来判定下一步的逻辑,要注意的点是waitForConfirmsOrDie方法如果返回false则会关闭channel,则接下来无法发送消息到broker

    • ps:spring.rabbitmq.publisher-confirms=true在2.20版本之后被弃用了

  • 自定义ConfirmCallback方法【需要先开启ConfirmCallback功能,即spring.rabbitmq.publisher-confirm-type不为none】

    • @PostConstruct表示当前配置类生效再执行

    • CorrelationData:用来表示当前消息唯一性,相当于id,需要自己指定

      rabbitTemplate.convertAndSend("hello-java-exchange","hello.java",
              orderEntity,new CorrelationData(UUID.randomUUID().toString()));
    @PostConstruct
    public void initRabbitTemplate(){
        rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
            @Override
            public void confirm(CorrelationData correlationData, boolean b, String s) {
                System.out.println("CorrelationData==>"+correlationData);//当前消息的唯一关联数据
                System.out.println("ack==>"+b);//是否成功,只要消息抵达服务器就成功
                System.out.println("failMessage==>"+s);//失败原因
            }
        });
    }
    • 循环依赖报错

      Relying upon circular references is discouraged and they are prohibited by default. Update your application to remove the dependency cycle between beans. As a last resort, it may be possible to break the cycle automatically by setting spring.main.allow-circular-references to true.
      • 解决:spring.main.allow-circular-references=true允许循环依赖

    image-20231216150619264

    .

returnCallback
  • confrim模式只能保证消息到达broker,不能保证消息准确投递到目标queue里

  • 有些业务场景需要保证消息一定要投递到目标queue里,此时就需要用到return退回模式

  • 如果未能投递到目标queue里将调用returnCallback ,可以记录下详细到投递数据,定期的巡检或者自动纠错都需要这些数据

  • 开启returnCallback功能

    • spring.rabbitmq.publisher-returns=true:开启发送端消息抵达队列确认

    • spring.rabbitmq.template.mandatory=true:只要抵达队列,就以异步模式优先回调

  • 自定义returnCallback方法,只要消息没有投递到指定队列才触发

    rabbitTemplate.setReturnsCallback(new RabbitTemplate.ReturnsCallback() {
        @Override
        public void returnedMessage(ReturnedMessage returnedMessage) {
            System.out.println(returnedMessage);
        }
    });
  • 测试发送一个错误的路由键

    ReturnedMessage [
        message=(Body:'[B@61901e73(byte[75])' MessageProperties [headers={__TypeId__=com.liao.gulimal.gulimalOrder.entity.OrderReturnReasonEntity},
    contentType=application/json, contentEncoding=UTF-8, contentLength=0, receivedDeliveryMode=PERSISTENT, priority=0, deliveryTag=0]), #投递失败的消息内容
    replyCode=312, #失败状态码
    replyText=NO_ROUTE, #失败原因
    exchange=hello-java-exchange, routingKey=hello1.java]
ack机制
  • 消费端确认,保证每个消息被正确消费后才被删除,queue无消费者,消息依然会被存储,直到消费者消费

  • 消费者获取到消息,成功处理,可以回复Ack给Broker

    • basic.ack用于肯定确认;broker将移除此消息

    • basic.nack用于否定确认;可以指定broker是否丢弃此消息,可以批量

    • basic.reject用于否定确认;同上,但不能批量

  • 消费者收到消息,默认会自动ack,只要消息抵达客户端就视为签收,但是无法确定此消息是否被处理完成

    • 问题举例:接收到很多消息并且都自动回复给服务器,但是只有部分数据处理成功就宕机了,其它消息都没处理完

  • 保证消息不丢失只能手动确认

    • 消息处理成功,ack(),接受下一个消息,此消息broker就会移除

    • 消息处理失败,nack()/reject(),重新发送给其他人进行处理,或者容错处理后ack

    • 消息一直没有调用ack/nack方法,broker认为此消息正在被处理,不会投递给别人,此时客户端断开,消息不会被broker移除

  • 开启手动确认收货:spring.rabbitmq.listener.simple.acknowledge-mode=manual

  • 队列中的消息成功抵达客户端,但是如果没有签收,会以未签收状态重新入队,即使消费者宕机,消息也不会消失,下次有消费者来读取,队列中的消息会重新变为ready状态

    image-20231216152745432

    image-20231216153016345

  • 签收货物需要用Channel接收,然后调用basicAck方法

    • 需要传递当前消息派发的标识【Message中有封装,该标识是在当前通道内自增的】和是否批量签收

    public void recieveMessage(OrderReturnReasonEntity content, Message message,Channel channel) throws IOException {
        System.out.println("监听到了OrderReturnReasonEntity类型的消息" + content);
        channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
    }
  • basicNack和basicReject都可以拒收,basicNack可以批量拒收

    • 以basicNack为例,有三个参数,分别是派发标识,是否批量拒收,是否重新入队

    • 重新入队的消息会被重新消费,相当于没有消费者调用ack的情况

    System.out.println("监听到了OrderReturnReasonEntity类型的消息" + content);
    long deliveryTag = message.getMessageProperties().getDeliveryTag();
    if(deliveryTag%2==0){
        System.out.println("拒签");
        channel.basicNack(deliveryTag, false,true);
    }else {
        System.out.println("已经签收");
        channel.basicAck(deliveryTag,false);
    }

    image-20231216155930173

    .

RabbitMQ延时队列
场景

image-20240113153826799

.

  • 比如未付款订单,超过一定时间后,系统自动取消订单并释放占有物品

  • 常用解决方案:spring的 schedule 定时任务轮询数据库

    • 缺点

      • 消耗系统内存、增加了数据库的压力

      • 存在较大的时间误差,即定时任务的时效性问题

        image-20240113154542102

        .

    • 解决:rabbitmq的消息TTL和死信Exchange结合,订单超时会发送消息给消息队列,此时才会给消息队列监听到

核心概念
消息的TTL
  • 消息的TTL(Time To Live)就是消息的存活时间

  • RabbitMQ可以对队列和消息分别设置TTL

    • 对队列设置就是队列没有消费者连接的保留时间

    • 也可以对每一个单独的消息做单独的设置,即没有消费该消息的保留时间

    • 超过了这个时间,认为这个消息/队列就死了,称之为死信

  • 如果队列设置了,消息也设置了,那么会取小的

    • 一个消息如果被路由到不同的队列中,这个消息死亡的时间有可能不一样(不同的队列设置)

    • 单个消息的TTL才是实现延迟任务的关键

    • 可以通过设置消息的expiration字段或者x-message-ttl属性来设置时间,两者是一样的效果

Dead Letter Exchanges
  • 一个消息在满足如下条件下【死信的情况】,会进死信路由,记住这里是路由而不是队列,一个路由可以对应很多队列

    • 一个消息被Consumer拒收了,并且reject方法的参数里requeue是false。不会被再次放在队列里让其他消费者使用

    • 上面的消息的TTL到了,消息过期了

    • 队列的长度限制满了,排在前面的消息会被丢弃或者扔到死信路由上

  • Dead Letter Exchange其实就是一种普通的exchange,和创建其他exchange没有两样

    • 只是在某一个设置Dead Letter Exchange的队列中有消息过期了,会自动触发消息的转发,发送到Dead Letter Exchange中去

  • 既可以控制消息在一段时间后变成死信,又可以控制变成死信的消息被路由到指定的交换机,结合二者就可以实现一个延时队列

    • 即死信队列只用于存放消息,不能被监听,当队列中的消息超时了就丢到消费队列

    • 这样子消费队列中存储的一定是超时的消息

  • 手动ack&异常消息统一放在一个队列处理建议的两种方式

    • catch异常后,手动发送到指定队列,然后使用channel给rabbitmq确认消息已消费

    • 给Queue绑定死信队列,使用nack(requque为false)确认消息消费失败

延时队列实现
实现方式
  • 设置队列过期时间实现延时队列

image-20240113160837714

.

  • 设置消息过期时间实现延时队列

    • 缺点:MQ采用惰性检查机制,放进定时队列的消息假设有三条,过期时间分别为5min、3min、1s,服务器弹出第一条数据,发现过期时间为5min,于是五分钟之后再重新检查,时间短的消息如果前面有时间长的消息阻塞着,就会有影响

image-20240113161210236

.

业务设计
  • 简单版

image-20240113162109520

.

  • 升级版

image-20240113163631880

.

环境搭建
  • springboot中Queue、Exchange、Binding可以使用@Bean注入到容器

    • 前提是MQ中没有创建这些组件,如果MQ中有这些组件,即使@Bean声明的属性发生变化也不会覆盖

    • 修改组件属性只能先删除MQ的组件,再@Bean注入

@Configuration
public class MyMQConfig {
    @Bean
    public Queue orderDelayQueue() {
        HashMap<String, Object> arguments = new HashMap<>();
        arguments.put("x-dead-letter-exchange", "order-event-exchange");//死信路由
        arguments.put("x-dead-letter-routing-key", "order.release.order");//死信的路由键
        arguments.put("x-message-ttl", 60000);//消息的过期时间
        //延时/死信队列
        Queue queue = new Queue("order.delay.queue", true, false, false, arguments);
        return queue;
    }
    @Bean
    public Queue orderReleaseQueue() {
        //消费队列
        Queue queue = new Queue("order.release.order.queue", true, false, false);
        return queue;
    }
    @Bean
    public Exchange orderEventExchange() {
        //topic类型的交换机,支持模糊匹配
        return new TopicExchange("order-event-exchange", true, false);
    }
    @Bean
    public Binding orderCreateOrderBingding() {
        //和order-event-exchange进行绑定,将路由键为order.create.order的消息路由到order.delay.queue【延时队列】
        return new Binding("order.delay.queue", Binding.DestinationType.QUEUE,
                "order-event-exchange","order.create.order",null);
    }
    @Bean
    public Binding orderReleaseOrderBingding() {
        //,将路由键为order.release.order的消息路由到order.release.order.queue【消费队列】
        return new Binding("order.release.order.queue", Binding.DestinationType.QUEUE,
                "order-event-exchange","order.release.order",null);
    }
}

image-20240113170708467

image-20240113170741573

测试
  • 监听消息的方法可以有三种参数(不分数量,顺序)Object content, Message message, Channel channel

  • channel可以用来拒绝消息,否则自动ack

  • 模拟下单成功发送消息

@ResponseBody
@GetMapping("/test/createOrder")
public String createOrder() {
    OrderEntity orderEntity = new OrderEntity();//模拟下单成功
    orderEntity.setOrderSn(UUID.randomUUID().toString());
    orderEntity.setCreateTime(new Date());
    //给MQ发送消息
    rabbitTemplate.convertAndSend("order-event-exchange","order.create.order",orderEntity);
    return "ok";
}
  • 接收消息

@RabbitListener(queues = "order.release.order.queue")
public void listerner(OrderEntity entity, Channel channel, Message message) throws IOException {
    System.out.println(entity.toString());
    channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);//签收消息
}

image-20240113172801934

.

消息可靠性考虑的问题
消息丢失
  • 消息发送出去,由于网络问题没有抵达服务器

    • 做好容错方法(try-catch),发送消息可能会网络失败,失败后要有重试机制,可记录到数据库,采用定期扫描重发的方式

    • 做好日志记录,每个消息状态是否都被服务器收到都应该记录【可以在对应微服务下创建消息日志表,保存消息的详细信息】

    CREATE TABLE `mq_message` (
        `message_id` char(32) NOT NULL, 
        `content` text, `to_exchane` varchar(255) DEFAULT NULL, 
        `routing_key` varchar(255) DEFAULT NULL, 
        `class_type` varchar(255) DEFAULT NULL, 
        `message_status` int(1) DEFAULT '0' COMMENT '0-新建 1-已发送 2-错误抵达 3-已抵达', 
        `create_time` datetime DEFAULT NULL, 
        `update_time` datetime DEFAULT NULL, 
        PRIMARY KEY (`message_id`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
    • 做好定期重发,如果消息没有发送成功,定期去数据库扫描未成功的消息进行重发

  • 消息抵达Broker,Broker要将消息写入磁盘(持久化)才算成功,此时Broker尚未持久化完成就宕机

    • publisher也必须加入确认回调机制,确认成功的消息,修改数据库消息状态

  • 自动ACK的状态下,消费者收到消息,但没来得及处理消息然后宕机

  • 一定开启手动ACK,消费成功才移除,失败或者没来得及处理就noAck并重新入队

  • 总结防止消息丢失的解决方法

    • 做好两端的消息确认机制【pulisher(成功回调确认),consumer(手动ack)】

    • 每个发送的消息都在数据库做好记录,定期将失败的消息重发

消息重复
场景
  • 消息消费成功,事务已经提交,ack时机器宕机,导致没有ack成功,Broker的消息重新由unack变为ready,并发送给其他消费者

  • 消息消费失败,由于重试机制,自动又将消息发送出去【业务允许的消息重复】

  • 成功消费,ack时宕机,消息由unack变为ready,Broker又重新发送【这和第一点不是一样的吗】

解决
  • 消费者的业务消费接口应该设计为幂等性的。比如扣库存有工作单的状态标志

  • 使用防重表(redis/mysql),发送消息每一个都有业务的唯 一标识,处理过就不用处理

  • RabbitMQ的每一个消息都有redelivered字段,可以获取是否是被重新投递过来的,而不是第一次投递过来的

message.getMessageProperties().getRedelivered();
消息积压
场景
  • 消费者宕机积压

  • 消费者消费能力不足积压

  • 发送者发送流量太大

解决
  • 发送端可以限制流量【后续会提到】

  • 消费端

    • 上线更多的消费者,进行正常消费

    • 上线专门的队列消费服务,将消息先批量取出来,记录数据库,离线慢慢处理

接口幂等性

定义
  • 接口幂等性就是用户对于同一操作发起的一次请求或者多次请求的结果是一致的,不会因为多次点击而产生了副作用

  • 比如说支付场景,用户购买了商品支付扣款成功,但是返回结 果的时候网络异常,此时钱已经扣了,用户再次点击按钮,此时会进行第二次扣款,返回结果成功,用户查询余额返发现多扣钱了,流水记录也变成了两条...,这就没有保证接口的幂等性

需要防止的场景
  • 用户多次点击按钮

  • 用户页面回退再次提交

  • 微服务互相调用,由于网络问题,导致请求失败,feign触发重试机制

  • 其他业务情况

幂等和非幂等举例
  • 以 SQL 为例,有些操作是天然幂等的

    • SELECT * FROM table WHER id=?,查询无论执行多少次都不会改变状态,是天然的幂等

    • UPDATE tab1 SET col1=1 WHERE col2=2,固定值更新无论执行成功多少次状态都是一致的,也是幂等操作

    • delete from user where userid=1,指定记录删除,执行多少次结果都一样,具备幂等性

    • insert into user(userid,name) values(1,'a') ,假如userid为唯一主键插入时指定固定主键,即重复操作上面的业务,只会插入一条用户数据,具备幂等性

  • 不为幂等的情况

    • UPDATE tab1 SET col1=col1+1 WHERE col2=2,每次执行的结果都会发生变化,不是幂等的

    • insert into user(userid,name) values(1,'a') ,假如userid不是主键,且可以重复,那上面业务多次操作,数据都会新增多条,不具备幂等性

幂等解决方案
token机制
步骤
  1. 服务端提供了发送 token 的接口。在分析业务的时候,哪些业务是存在幂等问题的, 就必须在执行业务前,先去获取 token,服务器会把 token 保存到 redis 中

  2. 然后调用业务接口请求时,把 token 携带过去,一般放在请求头部

  3. 服务器判断 token 是否存在 redis 中,存在表示第一次请求,然后删除 token,继续执行业务

  4. 如果判断 token 不存在 redis 中,就表示是重复操作,直接返回重复标记给 client,这样就保证了业务代码不被重复执行

危险性
  • 先删除token还是后删除token

    • 先删除可能导致,业务确实没有执行,重试还带上之前token,由于防重设计导致, 请求还是不能执行

    • 后删除可能导致,业务处理成功,但是服务闪断,出现超时,没有删除token,别人继续重试,导致业务被执行两遍

    • 最好设计为先删除 token,如果业务调用失败,就重新获取token再次请求

  • Token获取、比较和删除必须是原子性

    • redis.get(token) 、token.equals、redis.del(token)如果这两个操作不是原子,高并发下可能会get到同样的数据,判断都成功,继续业务并发执行

    • 可以在redis使用lua脚本实现if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end

各种锁机制
数据库悲观锁
  • select * from xxxx where id = 1 for update

  • 悲观锁使用时一般伴随事务一起使用,数据锁定时间可能会很长,需要根据实际情况选用

  • 另外要注意的是,id字段一定是主键或者唯一索引,不然可能造成锁表的结果,处理起来会非常麻烦

数据库乐观锁
  • 这种方法适合在更新的场景中, 乐观锁主要使用于处理读多写少的问题

  • update t_goods set count = count -1 , version = version + 1 where good_id=2 and version = 1

  • 根据version版本,也就是在操作库存前先获取当前商品的version版本号,然后操作的时候带上此 version 号

    • 第一次操作库存时,得到version为1,调用库存服务更新成功version就变成了2

    • 但返回给订单服务出现了问题,订单服务又一次发起调用库存服务,当订单服务传入的version还是1,再执行上面的sql 语句时,就不会执行

    • 因为version 已经变为2了,where条件就不成立,这样就保证了不管调用几次,只会真正的处理一次

业务层分布式锁
  • 如果多个机器可能在同一时间同时处理相同的数据,比如多台机器定时任务都拿到了相同数据处理,就可以加分布式锁,锁定此数据,处理完成后释放锁,获取到锁的必须先判断这个数据是否被处理过

各种唯一约束
数据库唯一约束
  • 插入数据,应该按照唯一索引进行插入,比如订单号,相同的订单就不可能有两条记录插入

  • 在数据库层面防止重复。 这个机制是利用了数据库的主键唯一约束的特性,解决了在 insert 场景时幂等问题

  • 主键的要求不是自增的主键,这样就需要业务生成全局唯一的主键

  • 如果是分库分表场景下,路由规则要保证相同请求下落地在同一个数据库和同一表中,要不然数据库主键约束就不起效果了,因为是不同的数据库和表主键不相关

redis的set防重
  • 很多数据需要处理,只能被处理一次

  • 比如可以计算数据的MD5将其放入redis的set, 每次处理数据,先看这个MD5是否已经存在,存在就不处理

防重表
  • 使用订单号orderNo做为去重表的唯一索引,把唯一索引插入去重表,再进行业务操作,且他们在同一个事务中

  • 这个保证了重复请求时,因为去重表有唯一约束,导致请求失败,避免了幂等问题

  • 需要注意的是,去重表和业务表应该在同一库中,这样就保证了在同一个事务,即使业务操作失败了,也会把去重表的数据回滚,这个很好的保证了数据一致性

  • 之前说的redis防重也算

全局请求唯一id
  • 调用接口时,生成一个唯一 id,redis将数据保存到集合中(去重),存在即处理过

  • 可以使用nginx设置每一个请求的唯一 id proxy_set_header X-Request-Id $request_id

分布式事务

事务基本概念
事务的基本性质
  • 数据库事务的几个特性:原子性(Atomicity )、一致性( Consistency )、隔离性( Isolation) 和持久性(Durabilily),简称就是 ACID

    • 原子性:一系列的操作整体不可拆分,要么同时成功,要么同时失败

    • 一致性:数据在事务的前后,业务整体一致【转账--A:1000;B:1000--》转 200事务成功;--》A:800 B:1200】

    • 隔离性:事务之间互相隔离

    • 持久性:一旦事务成功,数据一定会落盘在数据库

  • 在以往的单体应用中,我们多个业务操作使用同一条连接操作不同的数据表,一旦有异常, 我们可以很容易的整体回滚;

    image-20240110164201728

    .

    • Business:具体的业务代码

    • Storage:库存业务代码,扣库存

    • Order:订单业务代码,保存订单

    • Account:账号业务代码,减账户余额

    • 比如买东西业务,扣库存,下订单,账户扣款,是一个整体,必须同时成功或者失败

    • 一个事务开始,代表以上的所有操作都在同一个连接里面

事务的隔离级别
  • READ UNCOMMITTED(读未提交) 该隔离级别的事务会读到其它未提交事务的数据,此现象也称之为脏读

  • READ COMMITTED(读已提交) 一个事务可以读取另一个已提交的事务,多次读取会造成不一样的结果【即在当前事务执行期间,另一个事务进行多次提交】,此现象称为不可重复读问题,Oracle 和 SQL Server的默认隔离级别

  • REPEATABLE READ(可重复读) 该隔离级别是MySQL默认的隔离级别,在同一个事务里,select的结果是事务开始时时间点的状态

    • 同样的select操作读到的结果会是一致的,但是会有幻读现象

    • MySQL的InnoDB引擎可以通过next-key locks机制(参考下文"行锁的算法"一节)来避免幻读

  • SERIALIZABLE(序列化) 在该隔离级别下事务都是串行顺序执行的,MySQL 数据库的InnoDB引擎会给读操作隐式加一把读共享锁,从而避免了脏读、不可重读复读和幻读问题

  • @Transactional的isolation属性可以设置事务的隔离级别,越大的隔离级别并发性能更低

事务的传播行为
  • 事务传播行为指的就是当一个事务方法被另一个事务方法调用时,这个事务方法应该如何运行

    • 当两个方法共用一个事务时,被调用的事务方法设置的事务属性就失效了【共用调用者的事务属性

    • 同一个类下的方法互相调用,即使设置了不同的事务注解,依然以第一个执行的方法的事务为准

      • 原因是事务是用代理对象控制的

      • 解决方法

        1. 导入spring-boot-starter-aop

        2. @EnableTransactionManagement(proxyTargetClass = true)

        3. @EnableAspectJAutoProxy(exposeProxy=true)开启aspectj动态代理功能(即使没有接口也可以创建动态代理)

          • exposeProxy=true对外暴露代理对象

        4. 使用AopContext.currentProxy() 生成代理对象,通过代理对象调用其它方法

    image-20240110171310757

    .

  • PROPAGATION_REQUIRED:如果当前没有事务,就创建一个新事务,如果当前存在事务,就加入该事务,该设置是最常用的设置

  • PROPAGATION_SUPPORTS:支持当前事务,如果当前存在事务,就加入该事务,如果当 前不存在事务,就以非事务执行

  • PROPAGATION_MANDATORY:支持当前事务,如果当前存在事务,就加入该事务,如果 当前不存在事务,就抛出异常

  • PROPAGATION_REQUIRES_NEW:创建新事务,无论当前存不存在事务,都创建新事务

  • PROPAGATION_NOT_SUPPORTED:以非事务方式执行操作,如果当前存在事务,就把当 前事务挂起

  • PROPAGATION_NEVER:以非事务方式执行,如果当前存在事务,则抛出异常

  • PROPAGATION_NESTED:如果当前存在事务,则在嵌套事务内执行;如果当前没有事务,则执行与REQUIRED类似的操作

分布式事务引入

image-20240110160125105

.

  • 本地事务在分布式系统中只能控制自己的事务回滚,无法控制远程事务回滚

    • 订单服务在库存锁定之前抛异常,库存锁定不运行,全部回滚, 撤销操作

    • 库存服务锁定失败全部回滚,抛出的异常让订单感受到,订单服务也继续回滚

    • 库存服务锁定成功了,但是网络原因返回数据途中出问题【假失败】,导致订单服务回滚

    • 库存服务锁定成功了,库存服务下面的逻辑发生故障,订单回滚了,但是远程服务无法回滚

      • 利用消息队列实现最终一致 库存服务锁定成功后发给消息队列消息(当前库 存工作单),过段时间自动解锁,解锁时先查询 订单的支付状态

      • 解锁成功修改库存工作单详情项状态为已解锁,库存服务锁定库存,订单服务,下订单用户服务扣减积分

  • 分布式事务需要解决的问题

    • 远程服务假失败: 假设库存服务其实成功了,由于网络故障等没有返回,导致订单回滚,库存却扣减

    • 远程服务执行完成之后下面的其他方法出现问题 ,导致已执行的远程请求无法回滚

分布式事务概念
CAP定理
  • CAP原则又称CAP 定理,指的是在一个分布式系统中

    • 一致性(Consistency):在分布式系统中的所有数据备份在同一时刻是否同样的值(所有节点访问同一份最新的数据副本)

    • 可用性(Availability):在集群中一部分节点故障后,集群整体是否还能响应客户端的读写请求(对数据更新具备高可用性)

    • 分区容错性(Partition tolerance):大多数分布式系统都分布在多个子网络,每个子网络就叫做一个区(partition), 分区容错的意思是,区间通信可能失败。比如,一台服务器放在中国,另一台服务器放在美国,这就是两个区,它们之间可能无法通信

  • CAP 原则指的是,这三个要素最多只能同时实现两点,不可能三者兼顾

  • 一般来说,分区容错无法避免,因此可以认为CAP的P总是成立,剩下的 C 和 A 无法同时做

  • 对于多数大型互联网应用的场景,主机众多、部署分散,而且现在的集群规模越来越大,所以节点故障、网络故障是常态,而且要保证服务可用性达到 99.99999%,即保证 P 和 A,舍弃C

实现一致性的算法
  • 分布式系统中实现一致性有raft、paxos算法

  • Raft 【raft解释】

    • 集群中的节点有三种状态,leader、follower、candidate,假设节点都为follower状态,此时没有leader,那达到超时时间就会有节点变成候选人,通过选举产生leader,此时leader和客户端通信【leader选举】

    • 当客户端发送命令时,leader先将该命令记录在日志中,然后通知其它follower节点,当其它所有节点收到时且返回给leader之后,leader才执行命令,并且通知其它follower可以执行命令了【日志复制】

    • leader选举过程中有两个超时时间

      • 选举超时时间,follower变成一个候选人需要等待的时间【一般是150-300ms,又称节点自旋时间】,候选人会向其它节点发起投票通知【唱票】,其它节点投出票时会刷新自己的自旋时间【每个节点每轮只有一次投票,候选人会自投】

      • 心跳超时时间,leader每隔一段时间就要向其它随从发送心跳来维护心跳连接,follower收到消息会刷新自旋时间【心跳超时时间一定比节点自旋时间短】,当有一个新的随从变成候选者时【当前leader挂了等等】,停止发送心跳

      • 同票的情况会进行下一轮选举【follower收到一个候选人的通知就不会接收其它候选人的请求了】

    • 日志复制

      • 日志是在每一个心跳中发送出去的,在收到请求的下一个心跳携带日志数据

      • 大多数节点回复了(n/2+1),leader就会执行命令

      • 如果产生网络分区,不同分区有各自的领导

        • 下图两个分区中,上分区的领导可以收到大多数节点的恢复,所以会执行命令,下分区只写入日志不执行

        • 当恢复网络时,低轮次的领导会退位,所有跟老领导相关的操作都要回滚,然后和新领导同步

      image-20240110184848915

      ==》

      image-20240110184916725

BASE理论
  • 对CAP理论的延伸,思想是即使无法做到强一致性(CAP 的一致性就是强一致性),但可以采用适当的采取弱一致性,即最终一致性

  • BASE 是指基本可用(Basically Available)

    • 基本可用是指分布式系统在出现故障的时候,允许损失部分可用性(例如响应时间、 功能上的可用性),允许损失部分可用性

      • 响应时间上的损失:正常情况下搜索引擎需要在 0.5 秒之内返回给用户相应的查询结果,但由于出现故障(比如系统部分机房发生断电或断网故障),查询 结果的响应时间增加到了1~2秒

      • 功能上的损失:购物网站在购物高峰(如双十一)时,为了保护系统的稳定性, 部分消费者可能会被引导到一个降级页面

      • 需要注意的是,基本可用绝不等价于系统不可用

    • 软状态( Soft State)是指允许系统存在中间状态,而该中间状态不会影响系统整体可用性

      • 分布式存储中一般一份数据会有多个副本,允许不同副本同步的延时就是软状态的体现

      • mysql replication的异步复制也是一种体现

    • 最终一致性( Eventual Consistency)是指系统中的所有数据副本经过一定时间后,最终能够达到一致的状态

  • 强一致性、弱一致性、最终一致性

    • 对于关系型数据库,要求更新过的数据能被后续的访问都能看到,这是强一致性

    • 如果能容忍后续的部分或者全部访问不到,则是弱一致性

    • 如果经过一段时间后要求能访问到更新后的数据,则是最终一致性

    • 弱一致性和强一致性相反,最终一致性是弱一致性的一种特殊情况

分布式事务几种方案
刚性事务-2PC模式
  • 数据库支持的 2PC【2 phase commit 二阶提交】,又叫做 XA Transactions

  • MySQL 从 5.5 版本开始支持,SQL Server 2005 开始支持,Oracle 7 开始支持

  • XA是一个两阶段提交协议,该协议分为以下两个阶段

    • 第一阶段:事务协调器要求每个涉及到事务的数据库预提交(precommit)此操作,并反映是否可以提交

    • 第二阶段:事务协调器要求每个数据库提交数据

    • 如果有任何一个数据库否决此次提交,那么所有数据库都会被要求回滚它们在此事务中的那部分信息

image-20240110191448945

image-20240110191502392

  • XA 协议比较简单,而且一旦商业数据库实现了 XA 协议,使用分布式事务的成本也比较低

  • XA性能不理想,特别是在交易下单链路,往往并发量很高,XA 无法满足高并发场景

  • XA 目前在商业数据库支持的比较理想,在 mysql 数据库中支持的不太理想,mysql 的 XA 实现,没有记录 prepare 阶段日志,主备切换回导致主库与备库数据不一致

  • 许多 nosql 也没有支持 XA,这让 XA 的应用场景变得非常狭隘

  • 也有 3PC,引入了超时机制(无论协调者还是参与者,在向对方发送请求后,若长时间未收到回应则做出相应处理)

柔性事务
  • 刚性事务:遵循 ACID 原则,强一致性

  • 柔性事务:遵循 BASE 理论,最终一致性, 与刚性事务不同,柔性事务允许一定时间内,不同节点的数据不一致,但要求最终一致

TCC事务补偿型

image-20240110192511960

.

  • TCC 模式,是指支持把自定义的分支事务纳入到全局事务的管理中

    1. 一阶段 prepare 行为:调用自定义的 prepare 逻辑【预先准备】

    2. 二阶段 commit 行为:调用自定义的 commit 逻辑【通知提交】

    3. 三阶段 rollback 行为:调用自定义的 rollback 逻辑【最关键,任何一个事务失败就全部回滚或者进行补偿】

最大努力通知型
  • 按规律进行通知,不保证数据一定能通知成功,但会提供可查询操作接口进行核对

  • 这种方案主要用在与第三方系统通讯时,比如调用微信或支付宝支付后的支付结果通知

  • 这种方案也是结合 MQ 进行实现,例如通过 MQ 发送 http 请求,设置最大通知次数,达到通知次数后即不再通知

  • 案例:银行通知、商户通知等(各大交易业务平台间的商户通知:多次通知、查询校对、对 账文件),支付宝的支付成功异步回调

可靠消息+最终一致性
  • 异步确保型

  • 业务处理服务在业务事务提交之前,向实时消息服务请求发送消息,实时消息服务只记录消息数据,而不是真正的发送

  • 业务处理服务在业务事务提交之后,向实时消息服务确认发送

  • 只有在得到确认发送指令后,实时消息服务才会真正发送

  • 防止消息丢失

    • 做好消息确认机制(pulisher,consumer【手动 ack】)

    • 每一个发送的消息都在数据库做好记录,定期将失败的消息再次发送一 遍

CREATE TABLE `mq_message` (
    `message_id` char(32) NOT NULL, 
    `content` text, `to_exchane` varchar(255) DEFAULT NULL, 
    `routing_key` varchar(255) DEFAULT NULL, 
    `class_type` varchar(255) DEFAULT NULL, 
    `message_status` int(1) DEFAULT '0' COMMENT '0-新建 1-已发送 2-错误抵达 3-已抵达', 
    `create_time` datetime DEFAULT NULL, 
    `update_time` datetime DEFAULT NULL, 
    PRIMARY KEY (`message_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4

Seata

基础知识
简介
  • Seata 是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务

  • 官方文档:Apache Seata

核心概念
  • TC - 事务协调者:维护全局和分支事务的状态,驱动全局事务提交或回滚

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

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

image-20240112141217536

.

快速入门
  1. 为需要开启分布式事务的微服务都创建回滚日志表【SEATA AT 模式需要 UNDO_LOG 表】

    • 事务开始会先保存当前记录的状态,回滚就恢复到该状态

    -- 注意此处0.3.0+ 增加唯一索引 ux_undo_log
    CREATE TABLE `undo_log` (
      `id` bigint(20) NOT NULL AUTO_INCREMENT,
      `branch_id` bigint(20) NOT NULL,
      `xid` varchar(100) NOT NULL,
      `context` varchar(128) NOT NULL,
      `rollback_info` longblob NOT NULL,
      `log_status` int(11) NOT NULL,
      `log_created` datetime NOT NULL,
      `log_modified` datetime NOT NULL,
      `ext` varchar(100) DEFAULT NULL,
      PRIMARY KEY (`id`),
      UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
    ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
  2. 安装事务协调器seata-server

  3. 导入依赖,alibaba的seata场景驱动器会内嵌一个seata-server,版本与spring cloud版本有关,但是seata-server必须要和自己安装的服务器版本一致,所以需要排除并且自行导入,版本说明 · alibaba/spring-cloud-alibaba Wiki · GitHub

    <dependency>
        <groupId>io.seata</groupId>
        <artifactId>seata-spring-boot-starter</artifactId>
        <version>2.0.0</version>
    </dependency>
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
        <exclusions>
            <exclusion>
                <groupId>io.seata</groupId>
                <artifactId>seata-spring-boot-starter</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
  4. 所有需要用到分布式事务的微服务需要修改nacos的分组,要和seata-server的一致

    • 如果设置一致,网关的分组为DEFAULT_GROUP,这样子网关就识别不到SEATA_GROUP下的微服务了【会报java.net.UnknownHostException异常】,所以还是统一设置成DEFAULT_GROUP

    cloud:
      nacos:
        discovery:
          server-addr: 127.0.0.1:8848
          group: SEATA_GROUP
  5. @GlobalTransactional:标注在总事务中,其他分支事务,继续使用@Transactional即可

  6. 老版本的分支事务都需要使用seata DataSourceProxy代理自己的数据源,否则事务无法回滚【可以忽略

    @Configuration
    public class DataSourceConfig {
        @Bean
        @ConfigurationProperties(prefix = "spring.datasource")
        public DruidDataSource druidDataSource() {
            return new DruidDataSource();//先注入原本配置的数据源
        }
        /**
        * 需要将 DataSourceProxy 设置为主数据源,否则事务无法回滚
        *
        * @param druidDataSource The DruidDataSource * @return The default datasource */
        @Primary
        @Bean("dataSource")
        public DataSource dataSource(DruidDataSource druidDataSource) {
            return new DataSourceProxy(druidDataSource);//代理原本配置的数据源
        }
    }
seata-serveryaml配置
  • 视频来源动力节点

存储模式
  • store用于配置存储模式,SeataServer 需要对全局事务与分支事务进行存储,以便对它们进行管理,mode配置存储模式的类型

    seata:
        store:
         mode: db
         session:
           mode: db
         lock:
           mode: db
  • seata-server存储模式目前支持三种,不同模式的具体配置如下【配置和mode是同级的】

    • file模式:将相关数据存储在本地文件中,一般用于Seata Server的单机测试

      file:
        dir: sessionStore
        max-branch-session-size: 16384
        max-global-session-size: 512
        file-write-buffer-cache-size: 16384
        session-reload-read-size: 100
        flush-disk-mode: async
    • db模式:存储在数据库中,一般用于生产环境下的Seata Server集群部署,生产环境下使用最多的模式

      • 该模式相关的建表语句在安装目录的\script\server下【seata数据库需要自己创建】

      db:
        datasource: druid
        db-type: mysql
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://192.168.32.100:3306/seata?rewriteBatchedStatements=true&useSSL=false
        user: root
        password: 1212go12
        min-conn: 10
        max-conn: 100
        global-table: global_table
        branch-table: branch_table
        lock-table: lock_table
        distributed-lock-table: distributed_lock
        query-limit: 1000
        max-wait: 5000
    • redis模式:存储在redis 中,一般用于生产环境下的Seata Server集群部署,性能略高于db模式

      redis:
        mode: single
        # support: lua 、 pipeline
        type: lua
        database: 0
        min-conn: 10
        max-conn: 100
        password:
        max-total: 100
        query-limit: 1000
        single:
          host: 127.0.0.1
          port: 6379
        sentinel:
          master-name:
          sentinel-hosts:
          sentinel-password:
注册中心和配置中心
  • 配置nacos作为配置中心,seataServer.properties需要在nacos中创建发布并且指定SEATA_GROUP分组

    • 配置文件内容在安装目录的script\config-center\config.txt下,下面有seataServer.properties的详解

seata:
  config:
    # support: nacos 、 consul 、 apollo 、 zk  、 etcd3
    type: nacos #
    nacos:
      server-addr: 127.0.0.1:8848
      # 如果在nacos上添加了命名空间,则配置命令空间ID
      namespace: 
      # 配置分组
      group: SEATA_GROUP
      context-path:
      ##if use MSE Nacos with auth, mutex with username/password attribute
      #access-key:
      #secret-key:
      data-id: seataServer.properties
  • 配置nacos作为注册中心

    • 之前seata启动成功但是nacos一直识别不到是因为没有创建对应的命名空间,如果不指定就会放到默认的public空间中

seata:
 registry:
    # support: nacos 、 eureka 、 redis 、 zk  、 consul 、 etcd3 、 sofa
    type: nacos
    preferred-networks: 30.240.*
    nacos:
      application: seata-server
      server-addr: 127.0.0.1:8848
      group: SEATA_GROUP
      namespace:
      cluster: default
      context-path:
      ##if use MSE Nacos with auth, mutex with username/password attribute
      #access-key:
      #secret-key:
seataServer.properties
  • 该文件放在安装目录下的script\config-center\config.txt

  • seataServer.properties的service.vgroupMapping.default_tx_group=defaultservice.default以及seata配置文件的cluster: default三者是相互关联的,如果已经有注册中心了可以忽略这些配置

  • 存储模式需要和seata配置的一致,如果指定了某个存储模式,需要把该文件中另外两个模式的配置移除

#For details about configuration items, see https://seata.io/zh-cn/docs/user/configurations.html
#Transport configuration, for client and server
transport.type=TCP
transport.server=NIO
transport.heartbeat=true
transport.enableTmClientBatchSendRequest=false
transport.enableRmClientBatchSendRequest=true
transport.enableTcServerBatchSendResponse=false
transport.rpcRmRequestTimeout=30000
transport.rpcTmRequestTimeout=30000
transport.rpcTcRequestTimeout=30000
transport.threadFactory.bossThreadPrefix=NettyBoss
transport.threadFactory.workerThreadPrefix=NettyServerNIOWorker
transport.threadFactory.serverExecutorThreadPrefix=NettyServerBizHandler
transport.threadFactory.shareBossWorker=false
transport.threadFactory.clientSelectorThreadPrefix=NettyClientSelector
transport.threadFactory.clientSelectorThreadSize=1
transport.threadFactory.clientWorkerThreadPrefix=NettyClientWorkerThread
transport.threadFactory.bossThreadSize=1
transport.threadFactory.workerThreadSize=default
transport.shutdown.wait=3
transport.serialization=seata
transport.compressor=none
​
#Transaction routing rules configuration, only for the client
service.vgroupMapping.default_tx_group=default
#If you use a registry, you can ignore it
service.default.grouplist=127.0.0.1:8091
service.enableDegrade=false
service.disableGlobalTransaction=false
​
client.metadataMaxAgeMs=30000
#Transaction rule configuration, only for the client
client.rm.asyncCommitBufferLimit=10000
client.rm.lock.retryInterval=10
client.rm.lock.retryTimes=30
client.rm.lock.retryPolicyBranchRollbackOnConflict=true
client.rm.reportRetryCount=5
client.rm.tableMetaCheckEnable=true
client.rm.tableMetaCheckerInterval=60000
client.rm.sqlParserType=druid
client.rm.reportSuccessEnable=false
client.rm.sagaBranchRegisterEnable=false
client.rm.sagaJsonParser=fastjson
client.rm.tccActionInterceptorOrder=-2147482648
client.rm.sqlParserType=druid
client.tm.commitRetryCount=5
client.tm.rollbackRetryCount=5
client.tm.defaultGlobalTransactionTimeout=60000
client.tm.degradeCheck=false
client.tm.degradeCheckAllowTimes=10
client.tm.degradeCheckPeriod=2000
client.tm.interceptorOrder=-2147482648
client.undo.dataValidation=true
client.undo.logSerialization=jackson
client.undo.onlyCareUpdateColumns=true
server.undo.logSaveDays=7
server.undo.logDeletePeriod=86400000
client.undo.logTable=undo_log
client.undo.compress.enable=true
client.undo.compress.type=zip
client.undo.compress.threshold=64k
#For TCC transaction mode
tcc.fence.logTableName=tcc_fence_log
tcc.fence.cleanPeriod=1h
# You can choose from the following options: fastjson, jackson, gson
tcc.contextJsonParserType=fastjson
​
#Log rule configuration, for client and server
log.exceptionRate=100
​
#Transaction storage configuration, only for the server. The file, db, and redis configuration values are optional.
store.mode=db
store.lock.mode=db
store.session.mode=db
#Used for password encryption
#store.publicKey=
​
#These configurations are required if the `store mode` is `db`. If `store.mode,store.lock.mode,store.session.mode` are not equal to `db`, you can remove the configuration block.
store.db.datasource=druid
store.db.dbType=mysql
  driver-class-name: 
store.db.driverClassName=com.mysql.cj.jdbc.Driver
store.db.url=jdbc:mysql://192.168.32.100:3306/seata?rewriteBatchedStatements=true&useSSL=false
store.db.user=root
store.db.password=1212go12
store.db.minConn=5
store.db.maxConn=30
store.db.globalTable=global_table
store.db.branchTable=branch_table
store.db.distributedLockTable=distributed_lock
store.db.queryLimit=100
store.db.lockTable=lock_table
store.db.maxWait=5000
​
#Transaction rule configuration, only for the server
server.recovery.committingRetryPeriod=1000
server.recovery.asynCommittingRetryPeriod=1000
server.recovery.rollbackingRetryPeriod=1000
server.recovery.timeoutRetryPeriod=1000
server.maxCommitRetryTimeout=-1
server.maxRollbackRetryTimeout=-1
server.rollbackRetryTimeoutUnlockEnable=false
server.distributedLockExpireTime=10000
server.session.branchAsyncQueueSize=5000
server.session.enableBranchAsyncRemove=false
server.enableParallelRequestHandle=true
server.enableParallelHandleBranch=false
​
server.raft.cluster=127.0.0.1:7091,127.0.0.1:7092,127.0.0.1:7093
server.raft.snapshotInterval=600
server.raft.applyBatch=32
server.raft.maxAppendBufferSize=262144
server.raft.maxReplicatorInflightMsgs=256
server.raft.disruptorBufferSize=16384
server.raft.electionTimeoutMs=2000
server.raft.reporterEnabled=false
server.raft.reporterInitialDelay=60
server.raft.serialization=jackson
server.raft.compressor=none
server.raft.sync=true
​
#Metrics configuration, only for the server
metrics.enabled=false
metrics.registryType=compact
metrics.exporterList=prometheus
metrics.exporterPrometheusPort=9898

Sentinel

熔断降级限流
熔断
  • A 服务调用B服务的某个功能,由于网络不稳定问题或者B服务卡机,导致功能时间超长。如果这样子的次数太多,就可以直接将B断路(A 不再请求B接口),凡是调用B的直接返回降级数据,不必等待B的超长执行

  • 这样B的故障问题,就不会级联影响到 A

降级
  • 整个网站处于流量高峰期,服务器压力剧增,根据当前业务情况及流量,对一些服务和页面进行有策略的降级【停止服务,所有的调用直接返回降级数据】

  • 以此缓解服务器资源的的压力,以保证核心业务的正常运行,同时也保持了客户和大部分客户的得到正确的相应

熔断和降级异同
  • 相同点

    • 为了保证集群大部分服务的可用性和可靠性,防止崩溃,牺牲小我

    • 用户最终都是体验到某个功能不可用

  • 不同点

    • 熔断是被调用方故障,触发的系统主动规则

    • 降级是基于全局考虑,人工停止一些异常服务,释放资源

限流
  • 对打入服务的请求流量进行控制,使服务能够承担不超过自己能力的流量压力

Sentinel简介
Sentinel的特征
  • 丰富的应用场景:Sentinel 承接了阿里巴巴近10 年的双十一大促流量的核心场景,例如秒杀(即突发流量控制在系统容量可以承受的范围)、消息削峰填谷、集群流量控制、实时熔断下游不可用应用等

  • 完备的实时监控:Sentinel 同时提供实时的监控功能。您可以在控制台中看到接入 应用的单台机器秒级数据,甚至 500 台以下规模的集群的汇总运行情况

  • 广泛的开源生态:Sentinel 提供开箱即用的与其它开源框架/库的整合模块,例如 与 Spring Cloud、Dubbo、gRPC 的整合。您只需要引入相应的依赖并进行简单的配 置即可快速地接入 Sentinel

  • 完善的 SPI 扩展点:Sentinel 提供简单易用、完善的 SPI 扩展接口。您可以通过 实现扩展接口来快速地定制逻辑。例如定制规则管理、适配动态数据源等

image-20240126154837849

.

Sentinel分为两个部分
  • 核心库(Java 客户端)不依赖任何框架/库,能够运行于所有 Java 运行时环境,同时对Dubbo / Spring Cloud 等框架也有较好的支持

  • 控制台(Dashboard)基于 Spring Boot 开发,打包后可以直接运行,不需要额外的 Tomcat 等应用容器

Sentinel 基本概念
  • 资源

    • 是 Sentinel 的关键概念,它可以是Java应用程序中的任何内容

    • 例如由应用程序提供的服务,或由应用程序调用的其它应用提供的服务,甚至可以是一段代码

    • 在接下来的文档中,都会用资源来描述代码块。 只要通过Sentinel API 定义的代码,就是资源,能够被 Sentinel 保护起来

    • 大部分情况下, 可以使用方法签名,URL,甚至服务名称作为资源名来标示资源

  • 规则:围绕资源的实时状态设定的规则,可以包括流量控制规则、熔断降级规则以及系统保护规则,所有规则可以动态实时调整

Hystrix与Sentinel比较

image-20240125232423336

.

环境搭建
控制台环境搭建
  1. 导入依赖

    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
    </dependency>
  2. 下载sentinel控制台jar包【版本要和依赖版本一致】

  3. 在jar包所在位置进入命令行,启动控制台java -jar sentinel-dashboard-1.8.6.jar,访问sentinel控制台,账密都为sentinel

    image-20240126163550388

    .

  4. 导入控制台依赖的微服务需要配置控制台信息

    spring.cloud.sentinel.transport.dashboard=127.0.0.1:8080 #控制台所在端口
    spring.cloud.sentinel.transport.port=8719 #本服务和控制台通信的端口
  5. 启动项目,sentinel是懒启动,当监听的服务收到请求时才会显示规则信息

    • 所有规则默认存储在服务的内存中,服务关闭规则就会消失

    • 规则可以在对应的请求中点击按钮设置

    image-20240126170103045

    .

    • 在新增规则中,自行给请求绑定规则

    image-20240126170314625

    .

开启可视化实时监控
  1. 每个微服务导入审计功能,从而可以被实时监控

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
  1. 然后配置配置需要暴露/监控的资源

management.endpoints.web.exposure.include=* #配置暴露所有

image-20240127141050464

自定义流控响应
  • 配置web回调管理器【spring5以上的版本需要实现BlockExceptionHandler接口重写handle方法】

@Configuration
public class SentinelConfig implements BlockExceptionHandler {
    @Override
    public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, BlockException e) throws Exception {
        R error = R.error(BizCodeEnume.TO_MANY_REQUEST.getCode(), BizCodeEnume.TO_MANY_REQUEST.getMsg());
        httpServletResponse.setCharacterEncoding("UTF-8");//设置响应编码
        httpServletResponse.setContentType("application/json");//设置响应数据类型
        httpServletResponse.getWriter().write(JSON.toJSONString(error));
    }
}

image-20240127145128087

.

流量控制

image-20240127155513135

image-20240127155859596

  • QPS:每秒请求数

  • 集群阈值模式

    • 单机均摊:每台机器都不能超过单体均摊阈值

    • 总体阈值:集群中所有集群总请求不超过集群阈值

  • 流控模式

    • 直接:就是直接限制受控请求

    • 关联:需要指定关联资源,当关联资源流量大的时候就对受控资源进行限制

    • 链路:需要指定入口资源,流控只对从入口资源一连串调用到受控请求的路径生效

  • 流控效果

    • 直接拒绝/快速失败

    • Warm up(预热):需要指定预热时间,一点一点放入流量,达到预热时长才会放完阈值请求【不会让峰值流量全都涌进来】

      image-20240127160519383

      .

    • 排队等待:阈值外的请求就排队等待,可以设置超时时间,只要等待的请求超时了也直接失败

熔断降级
设计理念
  • Sentinel 和 Hystrix 的原则是一致的:当检测到调用链路中某个资源出现不稳定的表现,例如请求响应时间长或异常比例升高的时候,则对这个资源的调用进行限制,让请求快速失败, 避免影响到其它的资源而导致级联故障

  • 在限制的手段上,Sentinel 和 Hystrix 采取了完全不一样的方法

    • Hystrix 通过线程池隔离的方式来对依赖(在 Sentinel 的概念中对应 资源)进行了隔离

      • 好处是资源和资源之间做到了最彻底的隔离

      • 缺点是除了增加了线程切换的成本(过多的线程池导致线程数目过多),还需要预先给各个资源做线程池大小的分配

    • Sentinel 对这个问题采取了两种手段

      • 通过并发线程数进行限制

        • 和资源池隔离的方法不同,Sentinel 通过限制资源并发线程的数量,来减少不稳定资源对其它资源的影响

        • 这样不但没有线程切换的损耗,也不需要预先分配线程池的大小

        • 当某个资源出现不稳定的情况下,例如响应时间变长,对资源的直接影响就是会造成线程数的逐步堆积,当线程数在特定资源上堆积到一定的数量之后,对该资源的新请求就会被拒绝,堆积的线程完成任务后才开始继续接收请求

      • 通过响应时间对资源进行降级

        • 除了对并发线程数进行控制以外,Sentinel 还可以通过响应时间来快速降级不稳定的资源

        • 当依赖的资源出现响应时间过长后,所有对该资源的访问都会被直接拒绝,直到过了指定的时间,窗口之后才重新恢复

调用方的熔断保护
  1. 开启sentinel远程调用监控功能feign.sentinel.enabled=true

    image-20240127162149580

    .

  2. 在fegin远程调用接口中配置出错时调用的接口

    @FeignClient(value = "gulimall-seckill",fallback = SeckillFeginFailback.class)
    //SeckillFeginFailback秒杀服务的失败回调类
    @Slf4j
    @Component
    public class SeckillFeginFailback implements SeckillFeginService {
        @Override
        public R getSkuSeckillInfo(Long skuId) {
            log.error("秒杀服务---远程调用失败");
            return R.error(BizCodeEnume.TO_MANY_REQUEST.getCode(), BizCodeEnume.TO_MANY_REQUEST.getMsg());
        }
    }
  3. 模拟秒杀服务宕机,页面还可以正常访问

    image-20240127162832861

    image-20240127162845610

手动指定降级策略
降级策略
  • 一旦触发降级,就默认调用远程服务的熔断回调方法

  • 慢调用比例 (SLOW_REQUEST_RATIO)

    • 选择以慢调用比例作为阈值,需要设置允许的慢调用 RT(即最大的响应时间),请求的响应时间大于该值则统计为慢调用

    • 当单位统计时长(statIntervalMs)内请求数目大于设置的最小请求数目,并且慢调用的比例大于阈值,则接下来的熔断时长内请求会自动被熔断

    • 经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求响应时间小于设置的慢调用RT则结束熔断,若大于设置的慢调用RT则会再次被熔断

  • 异常比例 (ERROR_RATIO)

    • 当单位统计时长(statIntervalMs)内请求数目大于设置的最小请求数目,并且异常的比例大于阈值,则接下来的熔断时长内请求会自动被熔断

    • 经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。异常比率的阈值范围是 [0.0, 1.0],代表 0% - 100%

  • 异常数 (ERROR_COUNT)

    • 当单位统计时长内的异常数目超过阈值之后会自动进行熔断

    • 经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断

远程服务提供方降级
  • 超大流量的时候,必须牺牲一些远程服务

  • 尽管提供方【远程服务】被降级,调用方还是会调用该远程服务,被降级提供方依旧是在运行的,但是不运行自己的业务逻辑,默认返回降级后的数据

自定义受保护资源
抛出异常的方式
try(Entry entry= SphU.entry("资源名")) {
    ...
}catch (BlockException e){
    log.error("资源被限流,{}",e.getMessage());
}
  • 可以单独给自定义资源设置规则

image-20240127190443055

image-20240127190811881

基于注解
  • @SentinelResource("资源名")标识在需要受保护的方法上

image-20240127191104017

.

  • 受控会抛出异常

    Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.reflect.UndeclaredThrowableException] with root cause
    com.alibaba.csp.sentinel.slots.block.flow.FlowException: null
  • 可以设置blockHandler/fallback属性来指定限流/降级之后调用的方法

    • blockHandler是针对资源的回调

      • 除了方法名之外,其余的方法签名信息需要和限流方法的一致,而且一般要和原方法在同一个类

      • 返回值类型要和方法的参数列表需要和原方法一致,但是可以接收异常信息

    @SentinelResource(value = "getSkuSeckillInfoResource",blockHandler = "blockHandler" )
    public SeckillSkuRelationEntity getSkuSeckillInfo(Long skuId) {
        ...
    }    
    public SeckillSkuRelationEntity blockHandler(Long skuId,BlockException e){
        log.error("getSkuSeckillInfo方法被限流了{}",e.getMessage());
        return null;
    }

    image-20240127193538552

    .

    • fallback是针对所有类型异常的处理

      • 方法签名要求和blockHandler是一样的,可以额外接收异常信息

      • 处理的方法可以写在别的类中【用fallbackClass指定所在类,并且别的类处理方法需要指定为静态方法】

网关流控
环境搭建
  1. 导入依赖

    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-alibaba-sentinel-gateway</artifactId>
        <version>2021.0.5.0</version>
    </dependency>
  2. 默认监听网关中配置的路由id

    image-20240127195354691

    .

  3. 流控规则新增属性

    • 间隔:每隔一段时间进行统计,即一段时间间隔内不能超过指定的阈值

    • Burst size:额外允许的请求数目

    image-20240127195553508

    image-20240127195830025

    • 还可以根据请求属性进行控制

    image-20240127200239722

    .

  4. 可以自定义控制的api分组,

    image-20240127201441042

    image-20240127201544848

自定义流控返回
  • 这一块涉及spring5的响应式编程的内容

@Configuration
public class SentinelConfig {
    public SentinelConfig(){
        GatewayCallbackManager.setBlockHandler(new BlockRequestHandler() {
            //Mono Flux都是响应式编程的特性,参照spring5
            @Override
            public Mono<ServerResponse> handleRequest(ServerWebExchange serverWebExchange, Throwable throwable) {
                R error = R.error(BizCodeEnume.TO_MANY_REQUEST.getCode(), BizCodeEnume.TO_MANY_REQUEST.getMsg());
                String jsonString = JSON.toJSONString(error);
                Mono<ServerResponse> body = ServerResponse.ok().body(Mono.just(jsonString), String.class);
                return body;
            }
        });
    }
}

image-20240127203126120

.

Sleuth+Zipkin服务链路追踪

为什么用
  • 一个分布式系统往往有很多个服务单元,由于服务单元数量众多,业务的复杂性,如果出现了错误和异常,很难去定位

  • 一个请求可能需要调用很多个服务,而内部服务的调用复杂性,决定了问题难以定位

  • 所以必须实现分布式链路追踪,去跟进一个请求到底有哪些服务参与, 参与的顺序又是怎样的,从而达到每个请求的步骤清晰可见,出了问题,很快定位

  • 链路追踪组件有 Google 的 Dapper,Twitter的Zipkin,以及阿里的Eagleeye (鹰眼)等,它们都是非常优秀的链路追踪开源组件

基本术语
Span(跨度)
  • 基本工作单元,发送一个远程调度任务就会产生一个Span

  • Span是一个64位唯一标识的ID,Trace是用另一个64位ID来唯一标识

  • Span 还有其他数据信 息,比如摘要、时间戳事件、Span 的 ID、以及进度 ID

Trace(跟踪)
  • 一系列 Span组成的一个树状结构

  • 请求一个微服务系统的API接口需要调用多个微服务,调用每个微服务都会产生一个新的Span,所有由这个请求产生的Span组成了这个Trace

Annotation(标注)
  • 用来及时记录一个事件的,一些核心注解用来定义一个请求的开 始和结束

  • 这些注解包括以下

    • cs【Client Sent】:客户端发送一个请求,这个注解描述了这个Span的开始

    • sr【Server Received】:服务端获得请求并准备开始处理它,sr-cs便可得到网络传输的时间

    • ss【Server Sent】 (服务端发送响应):表明请求处理的完成(当请求返回客户端),ss-sr就可以得到服务器请求的时间

    • cr【Client Received】 (客户端接收响应):此时Span的结束,cr-cs便可以得到整个请求所消耗的时间

示例
  • 如果服务调用顺序如下

    image-20240128161646816

    .

    • 用以上概念完整的表示出来如下

      image-20240128161800636

      .

    • Span之间的父子关系如下

    image-20240128161909282

    .

整合Sleuth+zipkin
Sleuth环境搭建
  1. 服务提供者与消费者都要导入依赖【所以之间导入common模块】

    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-sleuth</artifactId>
    </dependency>
  2. 配置debug 日志

    logging.level.org.springframework.cloud.openfeign: debug
    logging.level.org.springframework.cloud.sleuth: debug
  3. 发起一次远程调用,观察控制台

zipkin环境搭建
  1. docker安装zipkin服务器docker run -d -p 9411:9411 openzipkin/zipkin

  2. 导入依赖【zipkin 依赖也同时包含了 sleuth,可以省略 sleuth 的引用】

    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-zipkin</artifactId>
        <version>2.2.8.RELEASE</version>
    </dependency>
  3. 添加zipkin相关配置

    zipkin:
      base-url: http://192.168.32.100:9411/ # zipkin 服务器的地址
    # 关闭服务发现,否则 Spring Cloud 会把 zipkin 的 url 当做服务名称
      discoveryClientEnabled: false
      sender:
        type: web # 设置使用 http 的方式传输数据
    sleuth:
      sampler:
        probability: 1 # 设置抽样采集率为 100%,默认为 0.1,即 10%

image-20240128171519954

.

zipkin数据持久化【了解】

image-20240128172142285

.

  • 使用es时 Zipkin Dependencies支持的环境变量

image-20240128172052502

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值