微服务入门(四)异步与线程池、MD5、RabbitMQ

创建线程的方式

初始化线程的 4 种方式
1) 、 继承 Thread
2) 、 实现 Runnable 接口
3) 、 实现 Callable 接口 + FutureTask (可以拿到返回结果, 可以处理异常)
4) 、 线程池

public static void main(Stringl] args) i
	System.out.println( "main....start....");

	Thread01 thread = new Thread01();
	thread.start();//启动线程
	System.out.print1n( "main...end....");
}
public static class Thread01 extends Thread{
	@Override
	public void run() {
	System.out.println( "当前线程: "+Thread.currentThread(). getId());
	inti=10/2;
	System.out.println("运行结果: "+i);
}


Runable01 runable01 = new Runable01( ) ;
new Thread( runable01).start();


public static class Runable01 implements Runnable{
	@Override
	public void run() {
		System.out.println("当前线程: "+Thread.currentThread().getId());
		inti=10/2;
		System.out.println("运行结果: "+i);
}

FutureTask<Integer> futureTask = new FutureTask<>(new Callable01());
new Thread(futureTask).start();
//阻塞等待整个线程执行完成,获取返回结果
Integer integer = futureTask.get();


public static class Callable01 implements Callable<Integer>{
	@Override
	public Integer call() throws Exception {
		System.out .print1n("当前线程: "+Thread.currentThread().getId());
		inti=10/2;
		System.out.println("运行结果: "+i);
		return i;
	}
}

 FutureTask<V> implements RunnableFuture<V> 
 public interface RunnableFuture<V> extends Runnable, Future<V> 

所以FutureTask其实就是继承Runnable,所以需要new Thread启动

以上三种方式在高并发下都不适合。
方式 1 和方式 2: 主进程无法获取线程的运算结果。
方式 3: 主进程可以获取线程的运算结果, 但是不利于控制服务器中的线程资源。 可能导致服务器资源耗尽。
线程池

//当前系统中池只有一两个,每个异步任务,提交给线程池让他自己去执行就行
ExecutorService service = Executors.newFixedThreadPool(10) ;
//另一种创建方式
new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, TimeUnit unit,
workQueue, threadFactory, handler);

通过线程池性能稳定, 也可以获取执行结果, 并捕获异常。 但是, 在业务复杂情况下, 一
个异步调用可能会依赖于另一个异步调用的执行结果
在这里插入图片描述
提交线程,submit可以获取返回结果,execute没有结果

new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, TimeUnit unit,
workQueue, threadFactory, handler);
/**
*七大参数
* corePoolSize:[5] 核心线程数[一直存在除非(allowCoreThreadTimeOut) ];线程池,创建好以后就准备就绪的线程数量,就等待来接受异步任务去执行
	5个Thread thread = new Thread();
	thread.start();
* maximumPoolsize:最大线程数量;控制资源
* keepAliveTime :存活时间。当线程空闲时间达到keepAliveTime,该线程会退出
* unit:时间单位
* BlockingQueue<Runnable> workQueue:阻塞队列。如果任务有很多,就会将目前多的任务放在队列里面。
只要有线程空闲,就会去队列里面取出新的任务继续执行。
* threadFactory :线程的创建工厂。
* RejectedExecut ionHandler handler:如果队列满了,按照我们指定的拒绝策略拒绝执行任务
*工作顺序:
* 1)、线程池创建,准备好core数量的核心线程,准备接受任务
* 1.1、core满了,就将再进来的任务放入阻塞队列中。空闲的core就会自己去阻塞队列获取任务执行
* 1.2、阻塞队列满了,就直接开新线程执行,最大只能开到max指定的数量
* 1.3、 max满了就用RejectedExecutionHandler拒绝任务
* 1.4、max都执行完成,有很多空闲.在指定的时间keepAliveTime以后, 释放max-core这些线程
* 1.3、max满了就用RejectedExecutionHandler拒绝任务
* 1.4、max都执行完成,有很多空闲. 在指定的时间keepAliveTime以后,释放max-core这些线程
*
*
new LinkedBlockingDeque<>(): 默认是Integer的最大值。内存不够,要指定最大数目

*问题:一个线程池core7;max20,queue:50,100并发进来怎么分配的;
* 7个会立即得到执行,50个会进入队列,再开13个进行执行。剩下的30个就使用拒绝策略。
*

*/
ThreadPoolExecutor executor = new ThreadPoolExecutor( corePoolSize: 5,
maximumPoolSize: 200,keepAliveTime: 10,TimeUnit .SECONDS ,
new LinkedBlockingDeque<>( capacity: 100000) ,Executors. defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());



Executors.newCachedThreadPool() core是0,所有都可回收。
Executors.newFixedThreadPool()固定大小,core=max; 都不可回收
Executors.newScheduledThreadPool()定时任务的线程池
Executors.newS ingleThreadExecutor().单线程的线程池,后台从队列里面获取任务,挨个执行。

开发中为什么使用线程池
1.降低资源的消耗
通过重复利用已经创建好的线程降低线程的创建和销毁带来的损耗
2.提高响应速度
因为线程池中的线程数没有超过线程池的最大上限时, 有的线程处于等待分配任务的状态, 当任务来时无需创建新的线程就能执行
3. 提高线程的可管理性
线程池会根据当前系统特点对池内的线程进行优化处理, 减少创建和销毁线程带来的系统开销。 无限的创建和销毁线程不仅消耗系统资源, 还降低系统的稳定性, 使用线程池进行统一分配
线程池使用强制
阿里巴巴对线程池使用进行了强制规范
在这里插入图片描述

CompletableFuture 异步编排

对于比较复杂的业务,有的需要并发运行,有的需要等待某一任务完成以后才执行,这样的编排,可以使用CompletableFuture ,这个是java 1.8增加的内容
创建异步任务
在这里插入图片描述

public static ExecutorService executor = Executors .newF ixedThreadPool( nThreads: 10);
public static void main(String[] args) throws ExecutionException, InterruptedException {
	System.out.println("main....start....");
//CompletableFuture<Void> future = CompletableFuture . runAsync(() -> {
//System. out . println("当前线程:”+ Thread. currentThread(). getId());
//inti=10/2;
//System. out.println("运行结果:”+ i);
//,executor);
	CompletableFuture<Integer> future = CompletableFuture. supplyAsync(() -> {
		System.out.println("当前线程:+ Thread.currentThread(). getId());
		int i=10/2;
		System.out.println("运行结果:+ i);
		return i;
	},executor);
	Integer integer = future.get();
	System. out.println("main....end...."+integer);
	}

计算完成时回调方法
在这里插入图片描述
whenComplete 可以处理正常和异常的计算结果, exceptionally 处理异常情况。
whenComplete 和 whenCompleteAsync 的区别:
whenComplete: 是执行当前任务的线程执行继续执行 whenComplete 的任务。
whenCompleteAsync: 是执行把 whenCompleteAsync 这个任务继续提交给线程池来进行执行。
方法不以 Async 结尾, 意味着 Action 使用相同的线程执行, 而 Async 可能会使用其他线程
执行(如果是使用相同的线程池, 也可能会被同一个线程选中执行)

CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() ->{
	System.out.println("当前线程:" +Thread.currentThread( ).getId());int i = 10 / 0;
	System.out.print1n(“运行结果:"+ i);return i;
},executor).whenComplete((res,excption)->i
	//虽然能得到异常信息,但是没法修改返回数据。
	System.out.println("异步任务成功完成了...结果是:"+res+" ;异常是: "+excption);}).exceptionally(throwable -> {
		//可以感知异常,同时返回默认值
		return 10;
});

handle 方法
在这里插入图片描述
和 complete 一样, 可对结果做最后的处理(可处理异常) , 可改变返回值。

//方法执行完成后的处理
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() ->{
	System.out.println("当前线程:" +Thread.currentThread().getId());
		int i = 10 / 0;
		system.out.print1n(“运行结果:" + i);return i;
},executor).handle( (res,thr)->{
		if(res!=nu1l){
			return res;
		}
		if(thr!=nu11){
			return 0;
		}
		return 0;

线程串行化方法
在这里插入图片描述
thenApply 方法: 当一个线程依赖另一个线程时, 获取上一个任务返回的结果, 并返回当前任务的返回值。
thenAccept 方法: 消费处理结果。 接收任务的处理结果, 并消费处理, 无返回结果。
thenRun 方法: 只要上面的任务执行完成, 就开始执行 thenRun, 只是处理完任务后, 执行thenRun 的后续操作
带有 Async 默认是异步执行的。 方法不以 Async 结尾, 意味着 Action 使用相同的线程执行, 而 Async 可能会使用其他线程执行
以上都要前置任务成功完成。
Function<? super T,? extends U>
T: 上一个任务返回结果的类型
U: 当前任务的返回值类型

/**
线程串行化
* 1)、thenRun: 不能获取到上一步的执行结果,无返回值
* . thenRunAsync(() -> {
System. out. println( "任务2启动了...");
}, executor);
* 2)、thenAcceptAsync;能接受上一 步结果,但是无返回值
* 3)、thenApplyAsync: ;能接受上一 步结果,有返回值*/
CompletableFuture<String> future = CompletableFuture . supplyAsync(() -> {
	System.out.println("当前线程:+ Thread. current Thread(). getId());
	inti=10/4;
	System.out.println( "运行结果:+ i);
	return i;
}, executor).thenApplyAsync(res -> {
	System.out.print1n( "任务2启动了...+ res);
	return "Hello+ res;
}, executor);


两任务组合 - 都要完成
在这里插入图片描述
在这里插入图片描述

两个任务必须都完成, 触发该任务。
thenCombine: 组合两个 future, 获取两个 future 的返回结果, 并返回当前任务的返回值
thenAcceptBoth: 组合两个 future, 获取两个 future 任务的返回结果, 然后处理任务, 没有返回值。
runAfterBoth: 组合两个 future, 不需要获取 future 的结果, 只需两个 future 处理完任务后,处理该任务

//两个都完成
CompletableFuture<Integer>.future01 = CompletableFuture. supplyAsync(() -> {
		System.out.print1n("任务1线程:+ Thread. current Thread().getId()); 
		int i=10/4
		System.out.println("任务1结束:);
		return i;
},executor);
CompletableFuture<String> future02 = CompletableFuture. supplyAsync(() -> {
		System.out.print1n("任务2线程:+ Thread. current Thread(). getId());
		System.out.println("任务2结束:);
		return "Hello" ;
}, executor);
	future01.runAfterBothAsync (future02, ()->{
		System.out.println("任务3开始...");
	}, executor);
// void accept(T t, U u);
	future01.thenAcceptBothAsync(future02,(f1,f2)->{
		System.out.println("任务3开始...之前的结果: "+f1+"--》 "+f2);
}, executor);

future01.thenCombineAsync(future02,(f1,f2)->{
	return f1 +"+f2+" -> Haha";
}, executor) ;


两任务组合 - 一个完成
在这里插入图片描述
在这里插入图片描述
当两个任务中, 任意一个 future 任务完成的时候, 执行任务。
applyToEither: 两个任务有一个执行完成, 获取它的返回值, 处理任务并有新的返回值。
acceptEither: 两个任务有一个执行完成, 获取它的返回值, 处理任务, 没有新的返回值。
runAfterEither: 两个任务有一个执行完成, 不需要获取 future 的结果, 处理任务, 也没有返回值。
多任务组合
在这里插入图片描述

MD5

Message Digest algorithm 5,信息摘要算法
• 压缩性:任意长度的数据,算出的MD5值长度都是固定的。
• 容易计算:从原数据计算出MD5值很容易。
• 抗修改性:对原数据进行任何改动,哪怕只修改1个字节,所得到的MD5值都有很大区别。
• 强抗碰撞:想找到两个不同的数据,使它们具有相同的MD5值,是非常困难的。
• 不可逆

加盐:
• 通过生成随机数与MD5生成字符串进行组合
• 数据库同时存储MD5值与salt值。验证正确性时使用salt进行MD5即可

单纯的使用MD5加密是不安全的,虽然不可逆,但是有彩虹表有可能破解,所以一般要加盐,就是加一个随机数,可以在数据库内保存一个盐的字段,把加密后的字段保存,然后把明文密码加上盐值,进行md5加密,与数据库中保存的加密字段进行比较
spring boot自带的加密方法比较好用,把盐值也加密到加密字段里,然后可以对比

BCryptPasswordEncoder pas swordEncoder = new BCryptPasswordEncoder();
//$2a$10$4IP4F/2iF02gbSvQKyJzGuI3RhU5Qdtr519KsyoXGAy. b7WT4P1RW
//$2a$10$iv6H6nqQ/NWOMkzgZSJdPeMOBGbnOayhZ9WAewOkOs sWScSHOgsAW
String encode = passwordEncoder.encode( rawPassword: "123456");
boolean matches = passwordEncoder.matches( rawPassword:123456",encodedPassword: "$2a$10$4IP4F/2iF02gbSvQKyJzGuI3RhU5Qdtr519KsyoXGAy.b7WT4P1RW");
System.out.println(encode+"=>"+matches);

RabbitMQ

安装
安装按照网上教程来就行了
Windows7系统安装RabbitMQ
安装成功以后,我碰到一个问题,命令行输入rabbitmq-server.bat,结果是

Starting broker… completed with 0 plugins.

我直接访问http://localhost:15672/访问不通,网上一搜才知道需要安装插件,在命令行按快捷键Ctrl+C,退出,然后输入

rabbitmq-plugins enable rabbitmq_management

然后输入rabbitmq-server.bat
在这里插入图片描述
出现这个结果,就可以访问了;http://localhost:15672/
在这里插入图片描述
用户名和密码都是guest
在这里插入图片描述
可以先看一篇文章,大概了解一下RabbitMQ。消息队列之 RabbitMQ

前言

消息中间件的作用
异步处理
先看一个场景
在这里插入图片描述
用户将注册信息写入数据库,然后再发送注册邮件,然后再发送注册短信,这样一个接一个的操作,需要全部完成才结束,这样的话,就会花费150ms
改进一下,使用异步
在这里插入图片描述
用户注册信息写入数据库,然后之前发送注册邮件和注册短信是一个接着一个操作的,现在改成异步操作,发送邮件和发送短信互不影响,这样就需要100ms
再改进
在这里插入图片描述
用户将注册信息写入数据库以后,然后再写入消息队列,然后就直接返回,这个流程就算结束,其他两个操作,发送邮件和发送短信,会从消息队列里边读取,至于何时发送,不在这个操作流程范围之内,这样响应时间就55ms。就跟抢火车票一样,支付完成就算结束,总不能得到邮件发送成功才结束吧,这样一个流程就会很长时间,如果涉及到秒杀,会更慢
应用解耦
在这里插入图片描述
订单系统与库存系统通过接口通信,这样如果订单系统接口修改,库存系统也要跟着修改
改进:
在这里插入图片描述
加入消息队列,通过订阅模式,订单系统把数据写到消息队列中,库存系统会得到通知,根据自己情况去获取,两者之间没有接口通讯,解耦
流量控制
QPS每秒查询率
在这里插入图片描述
如果没有消息队列,用户请求与秒杀业务直接关联,用户请求10000QPS(每秒查询率),秒杀系统只能承受6000QPS,这样就会崩溃,加入消息队列,请求放入消息队列(10000QPS)中,秒杀系统根据自己承受能力(6000QPS)去拉取数据,消息队列允许积压,等高峰过去,就能把积压消息消耗掉

概述

  1. 大多应用中,可通过消息服务中间件来提升系统异步通信、扩展解耦能力
  2. 消息服务中两个重要概念:
    消息代理(message broker)和目的地(destination)
    当消息发送者发送消息以后,将由消息代理接管,消息代理保证消息传递到指定目的地。
  3. 消息队列主要有两种形式的目的地
    队列(queue):点对点消息通信(point-to-point)
    主题(topic):发布(publish)/订阅(subscribe)消息通信
  4. 点对点式:
    • 消息发送者发送消息,消息代理将其放入一个队列中,消息接收者从队列中获取消息内容,消息读取后被移出队列
    • 消息只有唯一的发送者和接受者,但并不是说只能有一个接收者
  5. 发布订阅式:
    • 发送者(发布者)发送消息到主题,多个接收者(订阅者)监听(订阅)这个
    主题,那么就会在消息到达时同时收到消息
  6. JMS(Java Message Service)JAVA消息服务:
    • 基于JVM消息代理的规范。ActiveMQ、HornetMQ是JMS实现
  7. AMQP(Advanced Message Queuing Protocol)
    • 高级消息队列协议,也是一个消息代理的规范,兼容JMS
    • RabbitMQ是AMQP的实现
    在这里插入图片描述
  8. Spring支持
    • spring-jms提供了对JMS的支持
    • spring-rabbit提供了对AMQP的支持
    • 需要ConnectionFactory的实现来连接消息代理
    • 提供JmsTemplate、RabbitTemplate来发送消息
    • @JmsListener(JMS)、@RabbitListener(AMQP)注解在方法上监听消息
    代理发布的消息
    • @EnableJms、@EnableRabbit开启支持
  9. Spring Boot自动配置
    • JmsAutoConfiguration
    • RabbitAutoConfiguration
    10.市面的MQ产品
    • ActiveMQ、RabbitMQ、RocketMQ、Kafka

RabbitMQ概念

RabbitMQ简介:
RabbitMQ是一个由erlang开发的AMQP(Advanved Message Queue Protocol)的开源实现。
核心概念
Message
消息,消息是不具名的,它由消息头和消息体组成。消息体是不透明的,而消息头则由一系列的可选属性组成,这些属性包括routing-key(路由键)、priority(相对于其他消息的优先权)、delivery-mode(指出该消息可能需要持久性存储)等。
Publisher
消息的生产者,也是一个向交换器发布消息的客户端应用程序。
Exchange
交换器,用来接收生产者发送的消息并将这些消息路由给服务器中的队列。
Exchange有4种类型:direct(默认),fanout, topic, 和headers,不同类型的Exchange转发消息的策略有所区别
Queue
消息队列,用来保存消息直到发送给消费者。它是消息的容器,也是消息的终点。一个消息可投入一个或多个队列。消息一直在队列里面,等待消费者连接到这个队列将其取走。
Binding
绑定,用于消息队列和交换器之间的关联。一个绑定就是基于路由键将交换器和消息队列连接起来的路由规则,所以可以将交换器理解成一个由绑定构成的路由表。
Exchange 和Queue的绑定可以是多对多的关系。
Connection
网络连接,比如一个TCP连接。
Channel
信道,多路复用连接中的一条独立的双向数据流通道。信道是建立在真实的TCP连接内的虚拟连接,AMQP 命令都是通过信道发出去的,不管是发布消息、订阅队列还是接收消息,这些动作都是通过信道完成。因为对于操作系统来说建立和销毁 TCP 都是非常昂贵的开销,所以引入了信道的概念,以复用一条 TCP 连接。
Consumer
消息的消费者,表示一个从消息队列中取得消息的客户端应用程序。
Virtual Host
虚拟主机,表示一批交换器、消息队列和相关对象。虚拟主机是共享相同的身份认证和加
密环境的独立服务器域。每个 vhost 本质上就是一个 mini 版的 RabbitMQ 服务器,拥
有自己的队列、交换器、绑定和权限机制。vhost 是 AMQP 概念的基础,必须在连接时
指定,RabbitMQ 默认的 vhost 是 / 。
Broker
表示消息队列服务器实体

在这里插入图片描述

RabbitMQ运行机制

AMQP 中的消息路由
• AMQP 中消息的路由过程和 Java 开发者熟悉的 JMS 存在一些差别,AMQP 中增加了 Exchange 和
Binding 的角色。生产者把消息发布到 Exchange 上,消息最终到达队列并被消费者接收,而 Binding 决定交换器的消息应该发送到那个队列。
在这里插入图片描述
Exchange 类型
• Exchange分发消息时根据类型的不同分发策略有区别,目前共四种类型:direct、fanout、topic、headers 。headers 匹配 AMQP 消息的 header 而不是路由键,headers 交换器和 direct 交换器完全一致,但性能差很多,目前几乎用不到了,所以直接看另外三种类型
direct:(点对点完全匹配)
在这里插入图片描述
消息中的路由键(routing key)如果和Binding 中的 binding key 一致, 交换器就将消息发到对应的队列中。路由键与队列名完全匹配,如果一个队列绑定到交换机要求路由键为“dog”,则只转发 routing
key 标记为“dog”的消息,不会转发“dog.puppy”,也不会转发“dog.guard”等等。它是完全匹配、单播的模式。
fanout(发布订阅,广播,不区分路由键):
在这里插入图片描述
每个发到 fanout 类型交换器的消息都会分到所有绑定的队列上去。fanout 交换器不处理路由键,只是简单的将队列绑定到交换器上,每个发送到交换器的消息都会被转发到与该交换器绑定的所有队列上。很像子网广播,每台子网内的主机都获得了一份复制的消息。fanout 类型转发消息是最快的。
topic(匹配路由键,模糊匹配):
在这里插入图片描述
topic 交换器通过模式匹配分配消息的路由键属性,将路由键和某个模式进行匹配,此时队列需要绑定到一个模式上。它将路由键和绑定键的字符串切分成单词,这些单词之间用点隔开。它同样也会识别两个通配符:符号“#”和符号“*”。#匹配0个或多个单词,*匹配一个单词。
整体流程:
在这里插入图片描述
我的理解:

生产者和消费者互相通信,建立连接,生产者发送消息,消息包括头和体,最重要的是route-key,指定发给哪个交换机,哪个队列,消息来到Broker消息代理里指定的的虚拟主机,虚拟主机找到指定的交换机,交换机根据route-key,与queue消息队列的绑定关系,决定消息抵达哪个queue,消费者监听queue,内容会通过信道实时拿到,消费者通过Connection与Broker消息代理建立长连接,如果断开连接,消息会放回到对应的queue,防止丢失

RabbitMQ Management

在这里插入图片描述
主页面:左上角是一些版本号,右上角Refreshed ,是多久刷新一次,Virtual host是虚拟主机,User 用户信息
Overview:一些概览,包括Totals消息信息,Nodes结点信息,Churn statistics统计信息,Ports and contexts端口和上下文,Export definitions导出配置,Import definitions导入配置,方便配置从不同rabbitmq里切换
Connections:监控有多少客户端连接,一个客户端只建立一个连接
Channels:通道,一个连接有多个通道
Exchanges:交换机,列举交换机的名字,类型,特性,消息每秒进入速率,每秒发出的速率,可以添加新的交换机
Queues:消息队列,也可以新建队列
Admin:管理设置功能,可以设置用户,虚拟机之类的
最下角是一些参考信息
使用交换机和队列
在这里插入图片描述
Name:交换机名字
Type:交换机类型,在这里插入图片描述
Durability:持久化还是临时的,临时的,RabbitMQ一重启交换机就没了
Auto delete::是否自动删除,设置自动删除,交换机没有绑定队列就会删除
Internal:是否内部交换机,如果内部交换机,客户端就不能给交换机发送消息

交换机需要绑定队列,所以先创建队列
在这里插入图片描述
选择默认配置,创建一个最简单队列
在这里插入图片描述
交换机和队列绑定:
点击刚创建的交换机,点击Bindings
在这里插入图片描述
在这里插入图片描述
两个选项,交换机和交换机绑定,交换机和队列绑定
Routing key:路由键,我这边和名字设置成一样的了
然后点击绑定

可以设置多个queue,然后设置不同类型的交换机,绑定这些queue,然后交换机指定路由键,发送消息,测试效果
在这里插入图片描述

在这里插入图片描述
因为我是direct类型的交换机,与路由键精确匹配,
在这里插入图片描述
消息进入my.queue队列,点击队列,获取消息,
在这里插入图片描述
在这里插入图片描述
Nack message requeue true:获取消息,但是不做ack应答确认,消息重新入队
Ack message requeue false:获取消息,应答确认,消息不重新入队,将会从队列中删除
reject requeue true:拒绝获取消息,消息重新入队
eject requeue false:拒绝获取消息,消息不重新入队,将会被删除
带你及获取消息按钮
在这里插入图片描述
可以用不同模式来测试,观察队列中消息状态,有的模式消息删除,有的模式消息依然存在

可以测试一下fanout模式的交换机,就能看到发送消息与路由键无关,直接发送给所有绑定队列
topic模式的交换机,绑定多个队列,路由键可以使用xxx.#,或者*.xxx之类的路由键来匹配队列测试
在这里插入图片描述
在这里插入图片描述

与spring boot集成

pom.xml引入依赖

<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-amqp</artifactId>
		</dependency>

使用RabbitMQ
1、引入amqp场景;RabbitAutoConfiguration就会自动生效
2、给容器中自动配置了
RabbitTemplate、AmqpAdmin、CachingConnectionFactory、RabbitMessagingTemplate
AmqpAdmin:管理组件,RabbitTemplate:消息发送处理组件
所有的属性都是spring.rabbitmq开头
ConfigurationProperties(prefix = “spring.rabbitmq”)
public class RabbitProperties

3、@EnableRabbit : @EnabLeXxxx 开启什么功能
在Application上加入@EnableRabbit注解

点击RabbitAutoConfiguration,然后发现读取配置是RabbitProperties,以spring.rabbitmq开头
在这里插入图片描述
application.properties

spring.rabbitmq.host=192.168.56.10
spring.rabbitmq.port=5672
spring.rabbitmq.virtual-host=/
spring.rabbitmq.publisher-confirm-type=correlated
spring.rabbitmq.publisher-returns=true
spring.rabbitmq.template.mandatory=true
spring.rabbitmq.listener.simple.acknowledge-mode=manual

代码中创建交换机的参数,与用网页创建时的参数对应,比较好理解,比如

    public DirectExchange(String name, boolean durable, boolean autoDelete, Map<String, Object> arguments)

在这里插入图片描述
在代码中操作,可以在网页上查看,比较直观

	@Autowired
	AmqpAdmin amqpAdmin;

	@Autowired
	RabbitTemplate rabbitTemplate;

创建交换机

	@Test
	void createExchange() {
		DirectExchange directExchange = new DirectExchange("hello-java-exchange",true,false);
		amqpAdmin.declareExchange(directExchange);
		log.info("exchange:[{}]创建成功","hello-java-exchange");
	}

创建队列

@Test
	void createQueue() {
		Queue queue = new Queue("hello-java-queue",true,false,false);
		amqpAdmin.declareQueue(queue);
		log.info("queue:[{}]创建成功","hello-java-queue");
	}

建立连接

	@Test
	void createBinding(){
	//public Binding(String destination, Binding.DestinationType destinationType, String exchange, String routingKey, @Nullable Map<String, Object> arguments) {
	
	// (String destination【目的地】,
	//DestinationType destinationType【目的地类型】, String exchange【交换机】,
	// String routingKey【路由键】,
	//Map<String, 0bject> arguments【自定义参数)
//将exchange指定的交换机和destination目的地进行绑定,使用routingKey作为指定的路由键

		Binding binding = new Binding("hello-java-queue",Binding.DestinationType.QUEUE,
				"hello-java-exchange",
				"hello.java",null);
		amqpAdmin.declareBinding(binding);
		log.info("binding:[{}]创建成功","hello-java-binding");
	}

发送消息

@Test
	void sendMessageTest(){
	//1、发送消息
	rabbitTemplate.convertAndSend("hello-java-exchange" ,"hel1o.java" , "hello word!");

	}

发送消息,如果发送对象使用序列化机制,将对象写出去,对象必须实现Serializable

rabbitTemplate.convertAndSend("hello-java-exchange", "hello.java", entity);

默认实体类写出去,会变成byte数组,原因是RabbitTemplate类里MessageConverter 是SimpleMessageConverter,

    private MessageConverter messageConverter = new SimpleMessageConverter();

SimpleMessageConverter遇到Serializable,

 else if (object instanceof Serializable) {
            try {
                bytes = SerializationUtils.serialize(object);
            } catch (IllegalArgumentException var5) {
                throw new MessageConversionException("failed to convert to serialized Message content", var5);
            }

            messageProperties.setContentType("application/x-java-serialized-object");
        }

查看MessageConverter 的继承关系,idea中可以点击接口名,然后

ctrl+h

查看

public interface MessageConverter {

在这里插入图片描述

为了转换为Json,自己写个配置类,然后实现Jackson2JsonMessageConverter

@Configuration
public class MyMQConfig {
    @Bean
    public MessageConverter messageConverter() {
        return new Jackson2JsonMessageConverter();
    }
    }

@Bean放入容器中,容器中有,就用容器中的
监听消息
监听消息:使用@RabbitListcner;必须有@EnableRabbit
@RabbitListener:类+方法上(监听哪些队列即可)
@RabbitHandler:标在方法上(重载区分不同的消息)



/*queues:声明需要监听的所有队列
org.springframeworlk.amqp.core.Message*
*参数可以写以下类型
*1、第一个参数:Message message:原生消息详细信息。头+体
* 2、第二个参数:T<发送的消息的类型> 比如OrderReturnReasonEntity content;
* 可以自动转换成需要的类型
* 3、Channel channeL:当前传输数据的通道
Queue:可以很多人都来监听。只要收到消息,队列删除消息,而且只能有一个收到此消息场景:
1)、订单服务启动多个;同一个消息,只能有一个客户端收到
2)、只有一个消息完全处理完,方法运行结束,我们就可以接收到下一个消息

*/
@RabbitListener(queues = { "he1lo-java-queue"})
public void recieveMessage(Message message,OrderReturnReasonEntity content, Channel channel){
// { "id" :1 , " name" : "哈哈" , " sort" :nuLl , " status " :null , " createTime":1581144531744}byte[] body = message.getBody();
//消息头属性信息
MessageProperties properties = message.getMessageProperties();
System.out.print1n("接收到消息..."+message+"===>内容。"+content);

@RabbitHandler处理这样一个场景,同一个发送消息的方法,会发送不同类型的消息,这样在类上标注
@RabbitListener

@RabbitListener(queues = {"hello-java-queue"})//queues 声明需要监听的所有队列

类里接收不同类型的方法上标注@RabbitHandler

    @RabbitHandler
 public void reciveMessage(Message msg, OrderReturnReasonEntity content, Channel channel){
 }
  @RabbitHandler
   public void reciveMessage2(OrderEntity content){
   }

就可以根据不同的返回类型调用不同的方法

可靠抵达

怎样保证消息不丢失,可靠抵达?
在这里插入图片描述
publisher端:
confirmCallback :

消息只要被 broker 接收到就会执行 confirmCallback,如果是 cluster 模式,需要所有 broker
接收到才会调用 confirmCallback。 被 broker 接收到只能表示 message
已经到达服务器,并不能保证消息一定会被投递 到目标 queue 里。所以需要用到接下来的 returnCallback 。

returnCallback :

confrim 模式只能保证消息到达 broker,不能保证消息准确投递到目标 queue 里。在有
些业务场景下,我们需要保证消息一定要投递到目标 queue 里,此时就需要用到
return 退回模式。
这样如果未能投递到目标 queue 里将调用 returnCallback ,可以记录下详细到投递数
据,定期的巡检或者自动纠错都需要这些数据。

consumer端:
ack机制

消费者获取到消息,成功处理,可以回复Ack给Broker
• basic.ack用于肯定确认;broker将移除此消息
•basic.nack用于否定确认;可以指定broker是否丢弃此消息,可以批量
•basic.reject用于否定确认;同上,但不能批量
默认自动ack,消息被消费者收到,就会从broker的queue中移除
•queue无消费者,消息依然会被存储,直到消费者消费
•消费者收到消息,默认会自动ack。但是如果无法确定此消息是否被处理完成,或者成功处理。我们可以开启手动ack模式
•消息处理成功,ack(),接受下一个消息,此消息broker就会移除
•消息处理失败,nack()/reject(),重新发送给其他人进行处理,或者容错处理后ack
•消息一直没有调用ack/nack方法,broker认为此消息正在被处理,不会投递给别人,此时客户
端断开,消息不会被broker移除,会投递给别人

代码方面
设置配置

#开启发送端确认
spring .rabbitmq. publisher-confirms=true
#开启发送端消息抵达队列的确认
spring.rabbitmq.publisher-returns=true
#只要抵达队列,以异步方式优先回调我们这个returnconfirm
spring.rabbitmq.template.mandatory=true

发送端

@Configuration
public class MyRabbitConfig {

    @Autowired
    RabbitTemplate rabbitTemplate;

    /**
     * 使用JSON序列化机制,进行消息转换
     * @return
     */
    @Bean
    public MessageConverter messageConverter() {
        return new Jackson2JsonMessageConverter();
    }

    /*

        定制RabbitTemplate
        1、服务收到消息就回调
            1、spring .rabbitmq.pubLisher-confirms=true
            2、设置确认回调confirmCalLback
        2、消息正确抵达队列进行回调
            1、spring.rabbitmq.pubLisher-returns=true
                spring.rabbitmq.tempLate.mandatory=true
                2、设置确认回调ReturnCallback

     */
    @PostConstruct //MyRabbitConfig对象创建完成以后,执行这个方法
    public void initRabbitTemplate() {
        //设置确认回调
        rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
            /*只要消息抵达Broker就ack=true
            correlationData当前消息的唯一关联数据(消息的唯一id),
            ack消息是否成功收到
            cause失败原因
             */
            @Override
            public void confirm(@Nullable CorrelationData correlationData, boolean b, @Nullable String s) {
                System.out.println("confirm...CorrelationData[" + correlationData + "]==>[" + b + "]==>[" + s + "]");
            }
        });


        //设置消息抵达队列的确认回调
        rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback() {
            /*
            只要消息没有投递到指定队列,就触发这个失败回调
             */
    /** 只要消息没有投递给指定的队列,就触发这个失败回调*param message投递失败的消息详细信息
             @param repLycode回复的状态码
            @param replyText回复的文本内容
            @param exchange当时这个消息发给哪个交换机
           @param routingKey当时这个消息用哪个路由键
           */

            @Override
            public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
                System.out.println("Fail Message[" + message + "]==>replyCode[" + replyCode
                        + "]==>replyText[" + replyText + "]==>[exchange" + exchange + "]==>routingKey[" + routingKey + "]");
            }
        });
   }
}

消费端

   @RabbitHandler
   public void reciveMessage(Message msg, OrderReturnReasonEntity content, Channel channel){
       //消息头
       MessageProperties properties = msg.getMessageProperties();
       //消息体
       byte[] body = msg.getBody();
       //System.out.println("接收到消息...内容:"+msg+"==>类型"+content);
       //System.out.println("消息头:"+properties);
       //System.out.println("消息体:"+body);
       //Channel内按顺序自增
       long deliveryTag = msg.getMessageProperties().getDeliveryTag();
       //签收消息,multiple是否批量签收消息;拒签消息,requeue=true发回服务器,服务器重新入队,false丢弃消息
       try {
           if(deliveryTag%2==0){//模拟部分消息接收,其他的不接收
               channel.basicAck(deliveryTag,false);
               System.out.println("签收了消息..."+deliveryTag);
           }else {
            channel.basicNack(deliveryTag,false,false);
               System.out.println("拒签了消息..."+deliveryTag);
           }
       }catch (Exception e){
           //网络中断
       }
//
       System.out.println("接收到消息...内容:"+content);
   }
   @RabbitHandler
   public void reciveMessage2(OrderEntity content){
       System.out.println("接收到消息...内容:"+content);
   }

消费端ack:
1、默认是自动确认的,只要消息接收到,客户端会自动确认,服务端就会移除这个消息

问题:
我们收到很多消息,自动回复给服务器ack,只有一个消息处理成功,宕机了。发生消息丢失;
为保证消息不丢失,使用手动确认模式。只要我们没有明确告诉MQ,货物被签收。没有Ack,梢息就一直是unacked状态。即使consumer宕机。消息不会丢失,会重新变为Ready,下一次有新的Consumer连接进来就发给他
2、如何签收:
channel.basicAck(deliveryTag,false);签收;业务成功完成就应该签收
channel.basicNach( deliveryTag,false,true);拒签,业务失败,拒签

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值