rabbitMQ知识概括

129 阅读31分钟

@TOC

MQ简介

基于多线程队列简单实现mq:

public class MayiktThreadMQ {
    /**
     * Broker
     */
    private static LinkedBlockingDeque<JSONObject> broker = new 
    LinkedBlockingDeque<JSONObject>();

    public static void main(String[] args) {
        // 创建生产者线程
        Thread producer = new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    try {
                        Thread.sleep(1000);
                        JSONObject data = new JSONObject();
                        data.put("phone", "18611111111");
                        broker.offer(data);
                    } catch (Exception e) {

                    }

                }
            }
        }, "生产者");
        producer.start();
        Thread consumer = new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    try {
                        JSONObject data = broker.poll();
                        if (data != null) {
                            System.out.println(Thread.currentThread().getName() 
                            + ",获取到数据:" + data.toJSONString());
                        }
                    } catch (Exception e) {

                    }
                }
            }
        }, "消费者");
        consumer.start();
    }
}

基于netty实现mq:

  • 消费者netty客户端与nettyServer端MQ服务器端保持长连接,MQ服务器端保存消费者连接。
  • 生产者netty客户端发送请求给nettyServer端MQ服务器端,MQ服务器端在将该 消息内容发送给消费者。
  • 生产者投递消息给MQ服务器端,MQ服务器端需要缓存该消息。 在这里插入图片描述
  • 消息格式:body:{"msg":{"userId":"123456","age":"23"},"type":"producer",”topic”:””}
  • 如果mq服务器端宕机之后,消息如何保证不丢失? ①持久化机制。
  • 如果mq接收到生产者投递消息,如果消费者不在的情况下,该消息是否会丢失? ①不会丢失,消息确认机制 必须要消费者消费该消息成功之后,在通知给mq服务器端 删除该消息。
  • Mq服务器端将该消息推送消费者: ①消费者已经和mq服务器保持长连接,服务器自动推送。 ②消费者第一次刚启动的时候,消费者主动拉取消息
  • Mq如何实现抗高并发思想? ①Mq消费者根据自身能力情况 ,拉取mq服务器端消息消费。默认的情况下是取出一条消息。缺点:存在延迟的问题
  • 如何消费者提高速率: ①消费者实现集群。消费者批量获取消息即可。
  • 总结: ①rabbitMQ的总体架构思想就是使用Netty的NIO模型通信。生产者和消费者操作时首先要向消息中间件进行连接操作。也就是说生产者和消费者相对于消息中间件都是Client,而消息中间件是Server。

RabbitMQ简介

RabbitMQ介绍:

  • RabbitMQ是实现了高级消息队列协议(AMQP)的开源消息代理软件(亦称面向消息的中间件),
  • RabbitMQ服务器是用Erlang语言编写的。
  • 链接: ①RabitMQ官方网站RabbitMQ概念详解

RabbitMQ基础概念:

  • 通常我们谈到消息队列服务, 会有三个概念: 发消息者、消息队列、收消息者。RabbitMQ 在这个基本概念之上, 多做了一层抽象,在发消息者和队列之间, 加入了交换器(Exchange)。这样发消息者和消息队列就没有直接联系,转而变成发消息者把消息发给交换器,交换器根据调度策略再把消息转发给消息队列。
  • 消息生产者并没有直接将消息发送给消息队列,而是通过建立与Exchange的Channel,将消息发送给Exchange。Exchange根据路由规则,将消息转发给指定的消息队列。消息队列储存消息,等待消费者取出消息。消费者通过建立与消息队列相连的Channel,从消息队列中获取消息。
  • 概念名词: ①Channel(信道):多路复用连接中的一条独立的双向数据流通道。信道是建立在真实的TCP连接内的虚拟连接,复用TCP连接的通道。每个channel表示一个会话任务。Producer(消息的生产者):向消息队列发布消息的客户端应用程序。 ③Consumer(消息的消费者):从消息队列取得消息的客户端应用程序。 ④Message(消息):消息由消息头和消息体组成。消息体是不透明的,而消息头则由一系列的可选属性组成,这些属性包括routing-key(路由键)、priority(消息优先权)、delivery-mode(是否持久性存储)等。 ⑤Routing Key(路由键):消息头的一个属性,用于标记消息的路由规则,决定了交换机的转发路径。最大长度255 字节。 ⑥Queue(消息队列):存储消息的一种数据结构,用来保存消息,直到消息发送给消费者。它是消息的容器,也是消息的终点。一个消息可投入一个或多个队列。消息一直在队列里面,等待消费者连接到这个队列将消息取走。需要注意,当多个消费者订阅同一个Queue,这时Queue中的消息会被平均分摊给多个消费者进行处理,而不是每个消费者都收到所有的消息并处理,每一条消息只能被一个订阅者接收。 ⑦Exchange(交换器|路由器):提供Producer到Queue之间的匹配,接收生产者发送的消息并将这些消息按照路由规则转发到消息队列。交换器用于转发消息,它不会存储消息 ,如果没有 Queue绑定到 Exchange 的话,它会直接丢弃掉 Producer 发送过来的消息。交换器有四种消息调度策略(下面会介绍),分别是fanout, direct, topic, headers。 ⑧Binding(绑定):用于建立Exchange和Queue之间的关联。一个绑定就是基于Binding Key将Exchange和Queue连接起来的路由规则,所以可以将交换器理解成一个由Binding构成的路由表。 Binding Key(绑定键):Exchange与Queue的绑定关系,用于匹配Routing Key。最大长度255 字节。 ⑨Broker:RabbitMQ Server,服务器实体。 在这里插入图片描述
  • 关系: ①Producer与Exchange之间为一对多关系 ②Exchange与Queue之间为多对多关系 ③Queue与Consumer之间为多对多关系

RabbitMQ环境的基本安装:

  • 下载并安装erlang: ①链接:erlang下载地址 ②配置erlang环境变量信息: <1>新增环境变量ERLANG_HOME=erlang的安装地址。 <2>将%ERLANG_HOME%\bin加入到path中。 ③cmd下用erl -version检测是否配置成功。
  • 下载并安装RabbitMQ ①链接:RabbitMQ下载地址 ②配置环境变量: <1>新增环境变量RABBIT_HOME=rabbitmq的安装地址。 <2>将%RABBIT_HOME%\sbin加入到path中。 ③cmd下进入RabbitMQ Server目录下的sbin目录,用rabbitmqctl status检测RabbitMQ是否配置成功 ④打开sbin目录,双击rabbitmq-server.bat即可启动rabbitmq。 ⑤如果rabbitmq启动成功无法访问管理平台页面: <1>进入到F:\path\rabbitmq\rabbitmq\rabbitmq_server-3.6.9\sbin> <2>执行rabbitmq-plugins enable rabbitmq_management
  • Rabbitmq管理平台中心: ①地址 :http://127.0.0.1:15672 ②默认账号:guest/guest ,用户可以自己创建新的账号。
  • 注意:RabbitMQ 它依赖于Erlang,需要先安装Erlang。不同JDK版本对应的Erlang和RabbitMQ Server的版本也会有所不同。

RabbitMQ常见名词:

  • Virtual Hosts:像mysql有数据库的概念并且可以指定用户对库和表等操作的权限。RabbitMQ也有类似的权限管理。在RabbitMQ中可以虚拟消息服务器VirtualHost,每个VirtualHost相当于一个相对独立的RabbitMQ服务器,每个VirtualHost之间是相互隔离的,exchange、queue、message不能互通。
  • 默认的端口15672:rabbitmq管理平台端口号
  • 默认的端口5672: rabbitmq消息中间内部通讯的端口
  • 默认的端口号25672: rabbitmq集群的端口号

RabbitMQ信道channel概念:

  • 信道解释: ①信道是生产消费者与rabbit通信的渠道,生产者publish或者消费者消费一个队列都是需要通过信道来通信的。 ②信道是建立在TCP上面的虚拟链接,也就是rabbitMQ在一个TCP上面建立成百上千的信道来达到多个线程处理。 ③注意是一个TCP 被多个线程共享,每个线程对应一个信道,信道在rabbit都有唯一的ID,保证了信道的私有性,对应上唯一的线程使用。
  • 为什么RabbitMQ 需要信道,如果直接进行TCP通信呢? ①上述的描述其实已经很明显了,因为TCP可以被多个线程共享,显然线程比TCP要省事的多。 ②TCP的创建开销很大,创建需要三次握手,销毁需要四次握手。 ③如果不使用信道,那么引用程序就会使用TCP方式进行连接到RabbitMQ,因为MQ可能每秒会进行成千上万的链接, ④总之就是TCP消耗资源 ⑤TCP链接可以容纳无限的信道,不会有并发上面的性能瓶颈。
  • 在代码中并不会有直观的能看到信道这个概念,因为代码中都是用自动配置。 ①都是自动隐藏了详细的建立连接过程。 ②但是在使用rabbitmq时不管是消费还是生产都需要创建信道(channel) 和connection(连接)。 ③连接是连接到RabbitMQ的服务器。
@Autowired
RabbitTemplate rabbitTemplate; 
或者使用
@Autowired
private AmqpTemplate template;
  • 连接到RabbitMQ的示意图: 在这里插入图片描述

RabbitMQ详解

Exchange消息调度策略:

  • 调度策略是指Exchange在收到生产者发送的消息后依据什么规则把消息转发到一个或多个队列中保存。调度策略与三个因素相关:Exchange Type(Exchange的类型),Binding Key(Exchange和Queue的绑定关系),消息的标记信息(Routing Key和headers)。
  • Exchange根据消息的Routing Key和Exchange绑定Queue的Binding Key分配消息。生产者在将消息发送给Exchange的时候,一般会指定一个Routing Key,来指定这个消息的路由规则,而这个Routing Key需要与Exchange Type及Binding Key联合使用才能最终生效。
  • 在Exchange Type与BindingKey固定的情况下(一般这些内容都是固定配置好的),我们的生产者就可以在发送消息给Exchange时,通过指定Routing Key来决定消息流向哪里。
  • Exchange 类型: ①Fanout (订阅模式|广播模式):交换器会把所有发送到该交换器的消息路由到所有与该交换器绑定的消息队列中。订阅模式与Binding Key和Routing Key无关,交换器将接受到的消息分发给有绑定关系的所有消息队列队列(不论Binding Key和Routing Key是什么)。类似于子网广播,子网内的每台主机都获得了一份复制的消息。Fanout交换机转发消息是最快的。 在这里插入图片描述Direct(路由模式): <1>精确匹配:当消息的Routing Key与 Exchange和Queue 之间的Binding Key完全匹配,如果匹配成功,将消息分发到该Queue。只有当Routing Key和Binding Key完全匹配的时候,消息队列才可以获取消息。Direct是Exchange的默认模式。 <2>RabbitMQ默认提供了一个Exchange,名字是空字符串,类型是Direct,绑定到所有的Queue(每一个Queue和这个无名Exchange之间的Binding Key是Queue的名字)。所以,有时候我们感觉不需要交换器也可以发送和接收消息,但是实际上是使用了RabbitMQ默认提供的Exchange。 在这里插入图片描述Topic (通配符模式):按照正则表达式模糊匹配:用消息的Routing Key与 Exchange和Queue 之间的Binding Key进行模糊匹配,如果匹配成功,将消息分发到该Queue。 <1>#号表示支持匹配多个词; <2>*号表示只能匹配一个词 在这里插入图片描述Headers(键值对模式): <1>Headers不依赖于Routing Key与Binding Key的匹配规则来转发消息,交换器的路由规则是通过消息头的Headers属性来进行匹配转发的,类似HTTP请求的Headers。 <2>在绑定Queue与Exchange时指定一组键值对,键值对的Hash结构中要求携带一个键“x-match”,这个键的Value可以是any或all,代表消息携带的Hash是需要全部匹配(all),还是仅匹配一个键(any)。 <3>当消息发送到Exchange时,交换器会取到该消息的headers,对比其中的键值对是否完全匹配Queue与Exchange绑定时指定的键值对;如果完全匹配则消息会路由到该Queue,否则不会路由到该Queue。Headers交换机的优势是匹配的规则不被限定为字符串(String),而是Object类型。 在这里插入图片描述

RPC(不常用):

  • MQ本身是基于异步的消息处理,前面的示例中所有的生产者(P)将消息发送到RabbitMQ后不会知道消费者(C)处理成功或者失败,甚至连有没有消费者来处理这条消息都不知道。但实际的应用场景中,我们很可能需要一些同步处理,需要同步等待服务端将我的消息处理完成后再进行下一步处理。这相当于RPC(Remote Procedure Call,远程过程调用)。
  • RabbitMQ中实现RPC的机制是: ①生产者发送请求(消息)时,在消息的属性(MessageProperties,在AMQP协议中定义了14个属性,这些属性会随着消息一起发送)中设置两个属性值replyTo(一个Queue名称,用于告诉消费者处理完成后将通知我的消息发送到这个Queue中)和correlationId(此次请求的标识号,消费者处理完成后需要将此属性返还,生产者将根据这个id了解哪条请求被成功执行了或执行失败)。 ②消费者收到消息并处理。 ③ 消费者处理完消息后,将生成一条应答消息到replyTo指定的Queue,同时带上correlationId属性。 ④ 生产者之前已订阅replyTo指定的Queue,从中收到服务器的应答消息后,根据其中的correlationId属性分析哪条请求被执行了,根据执行结果进行后续业务处理。

消息应答(消费者):

  • 概念 : ①消费者完成一个任务可能需要一段时间,如果其中一个消费者处理一个长的任务并仅只完成 了部分突然它挂掉了,会发生什么情况。RabbitMQ 一旦向消费者传递了一条消息,便立即将该消 息标记为删除。在这种情况下,突然有个消费者挂掉了,我们将丢失正在处理的消息。以及后续 发送给该消费这的消息,因为它无法接收到。 ②为了保证消息在发送过程中不丢失,rabbitmq 引入消息应答机制,消息应答就是:消费者在接 收到消息并且处理该消息之后,告诉 rabbitmq 它已经处理了,rabbitmq 可以把该消息删除了。
  • 自动应答: ①消息发送后立即被认为已经传送成功,这种模式需要在高吞吐量和数据传输安全性方面做权 衡,因为这种模式如果消息在接收到之前,消费者那边出现连接或者 channel 关闭,那么消息就丢 失了,当然另一方面这种模式消费者那边可以传递过载的消息,没有对传递的消息数量进行限制, 当然这样有可能使得消费者这边由于接收太多还来不及处理的消息,导致这些消息的积压,最终 使得内存耗尽,最终这些消费者线程被操作系统杀死,所以这种模式仅适用在消费者可以高效并 以某种速率能够处理这些消息的情况下使用。
  • 消息应答的方法: ①Channel.basicAck(用于肯定确认):RabbitMQ 已知道该消息并且成功的处理消息,可以将其丢弃了 ②Channel.basicNack(用于否定确认) :不处理该消息了直接拒绝,可以将其丢弃了 ③Channel.basicReject(用于否定确认):不处理该消息了直接拒绝,可以将其丢弃了,与 Channel.basicNack 相比少一个Multiple参数
  • Multiple的解释: ①手动应答的好处是可以批量应答并且减少网络拥堵 ②multiple 的 true 和 false 代表不同意思 <1>true 代表批量应答 channel 上未应答的消息,比如说 channel 上有传送 tag 的消息 5,6,7,8 。当前 tag 是 8, 那么此时5-8 的这些还未应答的消息都会被确认收到消息应答。 <2>false 同上面相比只会应答 tag=8 的消息。 5,6,7 这三个消息依然不会被确认收到消息应答。
  • 消息自动重新入队:如果消费者由于某些原因失去连接(其通道已关闭,连接已关闭或 TCP 连接丢失),导致消息未发送 ACK 确认,RabbitMQ 将了解到消息未完全处理,并将对其重新排队。如果此时其他消费者可以处理,它将很快将其重新分发给另一个消费者。这样,即使某个消费者偶尔死亡,也可以确保不会丢失任何消息。
  • 总结: ①默认消息采用的是自动应答,所以我们要想实现消息消费过程中不丢失,需要把自动应答改为手动应答。Multiple参数一般也不开启,因为只能确保当前tag的消息处理成功,不能确保信道里当前tag之前的消息是否处理成功。

RabbitMQ 持久化:

  • 概念 :刚刚我们已经看到了如何处理任务不丢失的情况,但是如何保障当 RabbitMQ 服务停掉以后消息生产者发送过来的消息不丢失。默认情况下 RabbitMQ 退出或由于某种原因崩溃时,它忽视队列和消息,除非告知它不要这样做。确保消息不会丢失需要做两件事:我们需要将队列和消息都标 记为持久化。
  • 队列如何实现持久化 : ①之前我们创建的队列都是非持久化的,rabbitmq 如果重启的化,该队列就会被删除掉,如果 要队列实现持久化 需要在声明队列的时候把 durable 参数设置为持久化。 ②但是需要注意的就是如果之前声明的队列不是持久化的,需要把原先队列先删除,或者重新 创建一个持久化的队列,不然就会出现错误。
  • 消息实现持久化: ①要想让消息实现持久化需要在消息生产者修改代码,MessageProperties.PERSISTENT_TEXT_PLAIN 添加这个属性。 ②将消息标记为持久化并不能完全保证不会丢失消息。尽管它告诉 RabbitMQ 将消息保存到磁盘,但是这里依然存在当消息刚准备存储在磁盘的时候 但是还没有存储完,消息还在缓存的一个间隔点。此时并没有真正写入磁盘。持久性保证并不强,但是对于我们的简单任务队列而言,这已经绰绰有余了。如果需要更强有力的持久化策略。
  • 交换器实现持久化:交换器的持久化是在声明交换器的时候,将durable设置为true。如果交换器不设置持久化,那么在RabbitMQ交换器服务重启之后,相关的交换器信息会丢失,不过消息不会丢失,但是不能将消息发送到这个交换器。

工作队列及分发机制:

  • 为什么需要工作队列:工作队列(又称任务队列)的主要思想是避免立即执行资源密集型任务,而不得不等待它完成。相反我们安排任务在之后执行。我们把任务封装为消息并将其发送到队列。在后台运行的工作进程将弹出任务并最终执行作业。当有多个工作线程时,这些工作线程将一起处理这些任务。
  • 分发机制: ①轮询分发(Round-robin): <1>队列给每一个消费者发送数量一样的数据。 <2>如果工作队列中有两个消费者,两个消费者得到的数据量一样的,并不会因为两个消费者处理数据速度不一样使得两个消费者取得不一样数量的数据。 ②不公平分发: <1>在最开始的时候我们学习到 RabbitMQ 分发消息采用的轮训分发,但是在某种场景下这种策略并不是很好,比方说有两个消费者在处理任务,其中有个消费者 1 处理任务的速度非常快,而另外一个消费者 2 处理速度却很慢,这个时候我们还是采用轮训分发的化就会到这处理速度快的这个消费者很大一部分时间处于空闲状态,而处理慢的那个消费者一直在干活,这种分配方式在这种情况下其实就不太好,但是RabbitMQ 并不知道这种情况它依然很公平的进行分发,为了避免这种情况我们可以设置参数 channel.basicQos(1)。 ②意思就是如果这个任务我还没有处理完或者我还没有应答你,你先别分配给我,我目前只能处理一个任务,然后 rabbitmq 就会把该任务分配给没有那么忙的那个空闲消费者,当然如果所有的消费者都没有完成手上任务,队列还在不停的添加新任务,队列有可能就会遇到队列被撑满的情况,这个时候就只能添加新的 worker 或者改变其他存储任务的策略。
  • 预取值:本身消息的发送就是异步发送的,所以在任何时候,channel 上肯定不止只有一个消息另外来自消费者的手动确认本质上也是异步的。因此这里就存在一个未确认的消息缓冲区,因此希望开发人员能限制此缓冲区的大小,以避免缓冲区里面无限制的未确认消息问题。这个时候就可以通过使用 basic.qos (prefetchCount )方法设置“预取计数”值来完成的。

发布确认(生产者):

  • 发布确认原理: ①生产者将信道设置成 confirm 模式,一旦信道进入 confirm 模式,所有在该信道上面发布的消息都将会被指派一个唯一的 ID(从 1 开始),一旦消息被投递到所有匹配的队列之后,broker就会发送一个确认给生产者(包含消息的唯一 ID),这就使得生产者知道消息已经正确到达目的队列了,如果消息和队列是可持久化的,那么确认消息会在将消息写入磁盘之后发出,broker 回传给生产者的确认消息中 delivery-tag 域包含了确认消息的序列号,此外 broker 也可以设置basic.ack 的 multiple 域,表示到这个序列号之前的所有消息都已经得到了处理。 ②confirm 模式最大的好处在于他是异步的,一旦发布一条消息,生产者应用程序就可以在等信 道返回确认的同时继续发送下一条消息,当消息最终得到确认之后,生产者应用便可以通过回调 方法来处理该确认消息,如果 RabbitMQ 因为自身内部错误导致消息丢失,就会发送一条 nack 消 息,生产者应用程序同样可以在回调方法中处理该 nack 消息。
  • 开启发布确认的方法:发布确认默认是没有开启的,如果要开启需要调用方法 confirmSelect,每当你要想使用发布确认,都需要在 channel 上调用该方法。
  • 发布确认的策略: ①单个确认发布 : <1>这是一种简单的确认方式,它是一种同步确认发布的方式,也就是发布一个消息之后只有它 被确认发布,后续的消息才能继续发布,waitForConfirmsOrDie(long)这个方法只有在消息被确认 的时候才返回,如果在指定时间范围内这个消息没有被确认那么它将抛出异常。 <2>这种确认方式有一个最大的缺点就是:发布速度特别的慢,因为如果没有确认发布的消息就会 阻塞所有后续消息的发布,这种方式最多提供每秒不超过数百条发布消息的吞吐量。当然对于某 些应用程序来说这可能已经足够了。 ②批量确认发布 : <1>上面那种方式非常慢,与单个等待确认消息相比,先发布一批消息然后一起确认可以极大地 提高吞吐量,当然这种方式的缺点就是:当发生故障导致发布出现问题时,不知道是哪个消息出现 问题了,我们必须将整个批处理保存在内存中,以记录重要的信息而后重新发布消息。当然这种 方案仍然是同步的,也一样阻塞消息的发布。 ③异步确认发布: <1>异步确认虽然编程逻辑比上两个要复杂,但是性价比最高,无论是可靠性还是效率都没得说, 他是利用回调函数来达到消息可靠性传递的,这个中间件也是通过函数回调来保证是否投递成功, 下面就让我们来详细讲解异步确认是怎么实现的。 <2>如何处理异步未确认消息:最好的解决的解决方案就是把未确认的消息放到一个基于内存的能被发布线程访问的队列,比如说用 ConcurrentLinkedQueue 这个队列在 confirm callbacks 与发布线程之间进行消息的传递。 在这里插入图片描述
  • 以上 3 种发布确认速度对比: ①单独发布消息:同步等待确认,简单,但吞吐量非常有限。 ②批量发布消息:批量同步等待确认,简单,合理的吞吐量,一旦出现问题但很难推断出是那条 消息出现了问题。 ③异步处理:最佳性能和资源使用,在出现错误的情况下可以很好地控制,但是实现起来稍微难些。

RabbitMQ事务:

  • 事务的实现主要是对信道(Channel)的设置,主要的方法有三个: ①channel.txSelect()声明启动事务模式。 ②channel.txCommit()提交事务。 ③channel.txRollback()回滚事务。
  • springBoot-Rabbitmq配置事务:
//配置启用rabbitmq事务
@Bean("rabbitTransactionManager")
public RabbitTransactionManager rabbitTransactionManager(CachingConnectionFactory connectionFactory) {
   return new RabbitTransactionManager(connectionFactory);
}
  • 确保生产者发送消息不丢失:在通过channel.txSelect方法开启事务之后,我们便可以发布消息给RabbitMQ了,如果事务提交成功,则消息一定到达了RabbitMQ中,如果在事务提交之前由于RabbitMQ异常崩溃或者其他的原因抛出异常,这个时候我们可以将其捕获,进而通过执行channel.txRollback方法来实现事务回滚。
  • 注意:spring.rabbitmq.publisher-confirms一定要配置为false,否则会与事务处理相冲突,启动时会报异常。生产中不建议使用事务模式,性能比较低,尽量使用确认模式。
  • 交互流程: ①客户端发送给服务器Tx.Select(开启事务模式) ②服务器端返回Tx.Select-Ok(开启事务模式ok) ③推送消息 ④客户端发送给事务提交Tx.Commit ⑤服务器端返回Tx.Commit-Ok

RabbitMQ如何保证消息不丢失?

  • 生产者:使用同步或者异步的形式进行Ack 消息确认机制。 ①Confirms ②事务消息
  • 消费者: ①在rabbitmq情况下:必须要将消息消费成功之后,才会将该消息从mq服务器端中移除。 ②在kafka中的情况下:不管是消费成功还是消费失败,该消息都不会立即从mq服务器端移除。
  • Mq服务器端:在默认的情况下都会对队列中的消息实现持久化持久化硬盘。
  • 总结:使用消息确认机制+持久技术 ①消费者确认收到消息机制 生产者确认投递消息成功 使用Confirm机制 或者事务消息服务器端对队列中的消息实现持久化持久化硬盘

生产者如何获取消费结果:

  • 消费者消费成功结果:能够在数据库或缓存中插入一条数据 ①异步返回一个全局id,前端使用ajax定时主动查询;
  • Rocketmq 自带全局消息id,能够根据该全局消息获取消费结果 ①原理: 生产者投递消息到mq服务器,mq服务器端在这时候返回一个全局的消息id,当我们消费者消费该消息成功之后,消费者会给我们mq服务器端发送通知标记该消息消费成功。生产者获取到该消息全局id,每隔2s时间调用mq服务器端接口查询该消息是否有被消费成功。

基于amqp的rabbitmq中的点对点与发布订阅模型:

  • 实际上,用三种 Exchange 都可以实现点对点与发布订阅模型。 ①点对点模型: <1>direct Exchange:创建队列A,通过任意绑定键绑定到 Exchange,消息发送使用相同的绑定键 <2>fanout Exchange:创建队列A,绑定到 Exchange <3>topic Exchange:创建队列A,通过任意绑定键绑定到 Exchange,消息发送使用相同的绑定键 ②发布订阅模型: <1>direct Exchange:创建多个队列,通过为每个队列设定多个绑定,也能实现相对复杂的发布订阅模型 <2>fanout Exchange:创建多个队列,绑定到 Exchange,这是简化的发布订阅模型 <3>topic Exchange:创建多个队列,通过带通配符的绑定键实现复杂而又灵活的发布订阅模型
  • 简而言之AMQP 不按 JMS 那一套玩法玩。说白了AMQP 中所有的队列都遵照 JMS 中的点对点模型,发布订阅实质上是通过Exchange 的逻辑将消息复制到多个队列来实现的。

RabbitMQ中的六种队列:

  • 简单队列模式:最简单的工作队列,其中一个消息生产者,一个消息消费者,一个队列。也称为点对点模式。
  • 工作模式:一个消息生产者,一个交换器,一个消息队列,多个消费者,同样也称为点对点模式。
  • 发布/订阅模式:无选择接收消息,一个消息生产者,一个交换器,多个消息队列,多个消费者。称为发布/订阅模式。
  • 路由模式:在发布/订阅模式的基础上,有选择的接收消息,也就是通过 routing 路由进行匹配条件是否满足接收消息。
  • 主题模式:同样是在发布/订阅模式的基础上,根据主题匹配进行筛选是否接收消息,比第四类更灵活。
  • RPC模式:与上面其他5种所不同之处,类模式是拥有请求/回复的。也就是有响应的,上面5种都没有。

SpringBoot整合RabbitMQ

Maven依赖:

<!-- 添加springboot对amqp的支持 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>

配置类:

//RabbitMQConfig
@Component
public class RabbitMQConfig {
    //定义交换机
    private String EXCHANGE_SPRINGBOOT_NAME = "/mayikt_ex";
    //短信队列
    private String FANOUT_SMS_QUEUE = "fanout_sms_queue";
    //邮件队列
    private String FANOUT_EMAIL_QUEUE = "fanout_email_queue";

    //配置smsQueue
    @Bean
    public Queue smsQueue() {
        return new Queue(FANOUT_SMS_QUEUE);
    }

    // 配置emailQueue
    @Bean
    public Queue emailQueue() {
        return new Queue(FANOUT_EMAIL_QUEUE);
    }

    //配置fanoutExchange
    @Bean
    public FanoutExchange fanoutExchange() {
        return new FanoutExchange(EXCHANGE_SPRINGBOOT_NAME);
    }

    // 绑定交换机 sms
    @Bean
    public Binding bindingSmsFanoutExchange(Queue smsQueue, FanoutExchange fanoutExchange) {
        return BindingBuilder.bind(smsQueue).to(fanoutExchange);
    }

    // 绑定交换机 email
    @Bean
    public Binding bindingEmailFanoutExchange(Queue emailQueue, FanoutExchange fanoutExchange) {
        return BindingBuilder.bind(emailQueue).to(fanoutExchange);
    }
}

配置文件:

application.yml
spring:
  rabbitmq:
    ####连接地址
    host: 127.0.0.1
    ####端口号
    port: 5672
    ####账号
    username: guest
    ####密码
    password: guest
    ### 地址
    virtual-host: /meiteVirtualHosts

生产者:

//FanoutProducer
@RestController
public class FanoutProducer {
    @Autowired
    private AmqpTemplate amqpTemplate;

    //发送消息
    @RequestMapping("/sendMsg")
    public String sendMsg(String msg) {
        //1.交换机名称 2.路由key名称 3.发送内容
        amqpTemplate.convertAndSend("/mayikt_ex", "", msg);
        return "success";
    }
}

消费者:

//FanoutEmailConsumer
@Slf4j
@Component
@RabbitListener(queues = "fanout_email_queue")
public class FanoutEmailConsumer {

    @RabbitHandler
    public void process(String msg) {
        log.info(">>邮件消费者消息msg:{}<<", msg);
    }
}


//fanout_sms_queue
@Slf4j
@Component
@RabbitListener(queues = "fanout_sms_queue")
public class FanoutSmsConsumer {

    @RabbitHandler
    public void process(String msg) {
        log.info(">>短信消费者消息msg:{}<<", msg);
    }
}
  • 或者:
//FanoutEmailConsumer
@Slf4j
@Component
public class FanoutEmailConsumer {

    @RabbitListener(queues = "fanout_email_queue")
    public void process(String msg) {
        log.info(">>邮件消费者消息msg:{}<<", msg);
    }
}


//fanout_sms_queue
@Slf4j
@Component
public class FanoutSmsConsumer {

    @RabbitListener(queues = "fanout_sms_queue")
    public void process(String msg) {
        log.info(">>短信消费者消息msg:{}<<", msg);
    }
}

消费端消息重试机制:

  • 消息在消费端有可能在执行过程中出现异常。RabbitMQ提供了消息重试机制。如果消息出现异常,那么会把消息重新退回队列,然后再次消费该消息。但是重试的次数是有限的。
#重试机制设置
    listener:
      simple:
        retry:
          ####开启消费者重试
          enabled: true
          ####最大重试次数
          max-attempts: 5
          ####重试间隔秒数(3s)
          initial-interval: 3000	

Springboot整合rabbitmq配置详解:

# base
spring.rabbitmq.host: 服务Host
spring.rabbitmq.port: 服务端口
spring.rabbitmq.username: 登陆用户名
spring.rabbitmq.password: 登陆密码
spring.rabbitmq.virtual-host: 连接到rabbitMQ的vhost
spring.rabbitmq.addresses: 指定client连接到的server的地址,多个以逗号分隔(优先取addresses,然后再取host)
spring.rabbitmq.requested-heartbeat: 指定心跳超时,单位秒,0为不指定;默认60s
spring.rabbitmq.publisher-confirms: 是否启用【发布确认】
spring.rabbitmq.publisher-returns: 是否启用【发布返回】
spring.rabbitmq.connection-timeout: 连接超时,单位毫秒,0表示无穷大,不超时
spring.rabbitmq.parsed-addresses:

# ssl
spring.rabbitmq.ssl.enabled: 是否支持ssl
spring.rabbitmq.ssl.key-store: 指定持有SSL certificate的key store的路径
spring.rabbitmq.ssl.key-store-password: 指定访问key store的密码
spring.rabbitmq.ssl.trust-store: 指定持有SSL certificates的Trust store
spring.rabbitmq.ssl.trust-store-password: 指定访问trust store的密码
spring.rabbitmq.ssl.algorithm: ssl使用的算法,例如,TLSv1.1

# cache
spring.rabbitmq.cache.channel.size: 缓存中保持的channel数量
spring.rabbitmq.cache.channel.checkout-timeout: 当缓存数量被设置时,从缓存中获取一个channel的超时时间,单位毫秒;如果为0,则总是创建一个新channel
spring.rabbitmq.cache.connection.size: 缓存的连接数,只有是CONNECTION模式时生效
spring.rabbitmq.cache.connection.mode: 连接工厂缓存模式:CHANNEL 和 CONNECTION

# listener
spring.rabbitmq.listener.simple.auto-startup: 是否启动时自动启动容器
spring.rabbitmq.listener.simple.acknowledge-mode: 表示消息确认方式,其有三种配置方式,分别是none、manual和auto;默认auto
spring.rabbitmq.listener.simple.concurrency: 最小的消费者数量
spring.rabbitmq.listener.simple.max-concurrency: 最大的消费者数量
spring.rabbitmq.listener.simple.prefetch: 指定一个请求能处理多少个消息,如果有事务的话,必须大于等于transaction数量.
spring.rabbitmq.listener.simple.transaction-size: 指定一个事务处理的消息数量,最好是小于等于prefetch的数量.
spring.rabbitmq.listener.simple.default-requeue-rejected: 决定被拒绝的消息是否重新入队;默认是true(与参数acknowledge-mode有关系)
spring.rabbitmq.listener.simple.idle-event-interval: 多少长时间发布空闲容器时间,单位毫秒

spring.rabbitmq.listener.simple.retry.enabled: 监听重试是否可用
spring.rabbitmq.listener.simple.retry.max-attempts: 最大重试次数
spring.rabbitmq.listener.simple.retry.initial-interval: 第一次和第二次尝试发布或传递消息之间的间隔
spring.rabbitmq.listener.simple.retry.multiplier: 应用于上一重试间隔的乘数
spring.rabbitmq.listener.simple.retry.max-interval: 最大重试时间间隔
spring.rabbitmq.listener.simple.retry.stateless: 重试是有状态or无状态


# template
spring.rabbitmq.template.mandatory: 启用强制信息;默认false
spring.rabbitmq.template.receive-timeout: receive() 操作的超时时间
spring.rabbitmq.template.reply-timeout: sendAndReceive() 操作的超时时间
spring.rabbitmq.template.retry.enabled: 发送重试是否可用
spring.rabbitmq.template.retry.max-attempts: 最大重试次数
spring.rabbitmq.template.retry.initial-interval: 第一次和第二次尝试发布或传递消息之间的间隔
spring.rabbitmq.template.retry.multiplier: 应用于上一重试间隔的乘数
spring.rabbitmq.template.retry.max-interval: 最大重试时间间隔

RabbitMQ死信队列与延迟队列

死信队列简介:

  • 概念:先从概念解释上搞清楚这个定义,死信,顾名思义就是无法被消费的消息,字面意思可以这样理解,一般来说,producer 将消息投递到 broker 或者直接到 queue 里了,consumer 从 queue 取出消息进行消费,但某些时候由于特定的原因导致 queue 中的某些消息无法被消费,这样的消息如果没有后续的处理,就变成了死信,有死信自然就有了死信队列。
  • 死信的来源: ①消息 TTL 过期队列达到最大长度(队列满了,无法再添加数据到 mq 中)消息被拒绝(basic.reject 或 basic.nack)并且 requeue=false.
  • 代码架构图: 在这里插入图片描述

死信队列和普通队列区别:

  • 死信队列和普通队列区别不是很大。
  • 普通与死信队列都有自己独立的交换机和路由key、队列和消费者。
  • 区别: ①生产者投递消息先投递到我们普通交换机中,普通交换机在将该消息投到普通队列中缓存起来,普通队列对应有自己独立普通消费者。 ②如果生产者投递消息到普通队列中,普通队列发现该消息一直没有被消费者消费的情况下,在这时候会将该消息转移到死信(备胎)交换机中,死信(备胎)交换机对应有自己独立的 死信(备胎)队列 对应独立死信(备胎)消费者。

死信队列应用场景:

  • 为了保证订单业务的消息数据不丢失,需要使用到 RabbitMQ 的死信队列机制,当消息消费发生异常时,将消息投入死信队列中。
  • 例子:用户在商城下单成功并点击去支付后在指定时间未支付时自动失效
  • 具体操作:采用死信队列,创建一个普通队列没有对应的消费者消费消息,在30分钟过后就会将该消息转移到死信备胎消费者实现消费。备胎死信消费者会根据该订单号码查询是否已经支付过,如果没有支付的情况下 则会开始回滚库存操作。

延迟队列简介:

  • 延迟队列概念:延时队列,队列内部是有序的,最重要的特性就体现在它的延时属性上,延时队列中的元素是希望在指定时间到了以后或之前取出和处理,简单来说,延时队列就是用来存放需要在指定时间被处理的元素的队列。
  • 延迟队列就是死信队列当中的消息 TTL 过期类型。
  • 延迟队列使用场景(这些场景都有一个特点,需要在某个事件发生之后或者之前的指定时间点完成某一项任务): ①订单在十分钟之内未支付则自动取消 ②新创建的店铺,如果在十天内都没有上传过商品,则自动发送消息提醒。 ③用户注册成功后,如果三天内没有登陆则进行短信提醒。 ④用户发起退款,如果三天内没有得到处理则通知相关运营人员。 ⑤预定会议后,需要在预定的时间点前十分钟通知各个与会人员参加会议
  • 代码架构图:队列的TTL时间由生产者指定。 在这里插入图片描述

RabbitMQ 中的 TTL:

  • TTL 是什么呢?TTL 是 RabbitMQ 中一个消息或者队列的属性,表明一条消息或者该队列中的所有 消息的最大存活时间。
  • 单位是毫秒。换句话说,如果一条消息设置了 TTL 属性或者进入了设置 TTL 属性的队列,那么这 条消息如果在 TTL 设置的时间内没有被消费,则会成为"死信"。如果同时配置了队列的 TTL 和消息的TTL,那么较小的那个值将会被使用,有两种方式设置 TTL。

Rabbitmq 插件实现延迟队列:

  • RabbitMQ 只会检查第一个消息是否过期,如果过期则丢到死信队列,如果第一个消息的延时时长很长,而第二个消息的延时时长很短,第二个消息并不会优先得到执行。
  • 如果不能实现在消息粒度上的 TTL,并使其在设置的 TTL 时间及时死亡,就无法设计成一个通用的延时队列。
  • 可以通过安装延时队列插件实现消息粒度上的 TTL。

总结:

  • 延时队列在需要延时处理的场景下非常有用,使用 RabbitMQ 来实现延时队列可以很好的利用 RabbitMQ的特性,如:消息可靠发送、消息可靠投递、死信队列来保障消息至少被消费一次以及未被正 确处理的消息不会被丢弃。另外,通过 RabbitMQ集群的特性,可以很好的解决单点故障问题,不会因为 单个节点挂掉导致延时队列不可用或者消息丢失。
  • 当然,延时队列还有很多其它选择,比如利用 Java 的 DelayQueue,利用 Redis 的 zset,利用 Quartz 或者利用kafka 的时间轮,这些方式各有特点,看需要适用的场景。

RabbitMQ高级

RabbitMQ消息幂等性问题:

  • 概念:用户对于同一操作发起的一次请求或者多次请求的结果是一致的,不会因为多次点击而产生了副作用。举个最简单的例子,那就是支付,用户购买商品后支付,支付扣款成功,但是返回结果的时候网络异常,此时钱已经扣了,用户再次点击按钮,此时会进行第二次扣款,返回结果成功,用户查询余额发现多扣钱了,流水记录也变成了两条。在以前的单应用系统中,我们只需要把数据操作放入事务中即可,发生错误立即回滚,但是再响应客户端的时候也有可能出现网络中断或者异常等等。
  • 消息重复消费:消费者在消费 MQ 中的消息时,MQ 已把消息发送给消费者,消费者在给 MQ 返回 ack 时网络中断,故 MQ 未收到确认信息,该条消息会重新发给其他的消费者,或者在网络重连后再次发送给该消费者,但实际上该消费者已成功消费了该条消息,造成消费者消费了重复的消息。
  • 解决思路 :MQ 消费者的幂等性的解决一般使用全局 ID 或者写个唯一标识比如时间戳 或者 UUID 或者订单消费者消费 MQ 中的消息也可利用 MQ 的该 id 来判断,或者可按自己的规则生成一个全局唯一 id,每次消费消息时用该 id 先判断该消息是否已消费过。
  • 消费端的幂等性保障:在海量订单生成的业务高峰期,生产端有可能就会重复发生了消息,这时候消费端就要实现幂等性,这就意味着我们的消息永远不会被消费多次,即使我们收到了一样的消息。业界主流的幂等性有两种操作: ①唯一 ID+指纹码机制,利用数据库主键去重:指纹码:我们的一些规则或者时间戳加别的服务给到的唯一信息码,它并不一定是我们系统生成的,基本都是由我们的业务规则拼接而来,但是一定要保证唯一性,然后就利用查询语句进行判断这个 id 是否存在数据库中,优势就是实现简单就一个拼接,然后查询判断是否重复;劣势就是在高并发时,如果是单个数据库就会有写入性能瓶颈当然也可以采用分库分表提升性能,但也不是我们最推荐的方式。 ②利用 redis 的原子性去实现:利用 redis 执行 setnx 命令,天然具有幂等性。从而实现不重复消费

优先级队列:

  • 在我们系统中有一个订单催付的场景,我们的客户在天猫下的订单,淘宝会及时将订单推送给我们,如果在用户设定的时间内未付款那么就会给用户推送一条短信提醒,很简单的一个功能对吧,但是,tmall商家对我们来说,肯定是要分大客户和小客户的对吧,比如像苹果,小米这样大商家一年起码能给我们创造很大的利润,所以理应当然,他们的订单必须得到优先处理,而曾经我们的后端系统是使用 redis 来存放的定时轮询,大家都知道 redis只能用 List 做一个简简单单的消息队列,并不能实现一个优先级的场景,所以订单量大了后采用 RabbitMQ 进行改造和优化,如果发现是大客户的订单给一个相对比较高的优先级,否则就是默认优先级。
  • 解法:增加x-max-priority参数。
  • 注意:要让队列实现优先级需要做的事情有如下事情:队列需要设置为优先级队列,消息需要设置消息的优先级,消费者需要等待消息已经发送到队列中才去消费因为,这样才有机会对消息进行排序。

RabbitMQ之常用API

消费者确认机制:

  • 消费者确认:消费者确认或者说消费者应答指的是RabbitMQ需要确认消息到底有没有被收到。 ①RabbitMQ中的两种确认方式: <1>自动确认方式:RabbitMQ成功将消息发出(即将消息成功写入TCP Socket)中立即认为本次投递已经被正确处理,不管消费者端是否成功处理本次投递 <2>手动处理方式:消费者收到消息后,手动调用basic.ack/basic.nack/basic.reject后,RabbitMQ收到这些消息后,才认为本次投递成功
#自动应答
boolean autoAck = true;
channel.basicConsume(QUEUE_NAME, autoAck, consumer);

#手动应答
boolean autoAck = false;
channel.basicConsume(QUEUE_NAME, autoAck, consumer);

消息消费:

  • 单一消费
channel.basicAck(envelope.getDeliveryTag(), false)
  • 批量消费 ①deliveryTag:该消息的index ②multiple:是否批量.true:将一次性ack所有小于deliveryTag的消息。
channel.basicAck(envelope.getDeliveryTag(), true)
  • basicNack ①deliveryTag:该消息的index ②multiple:是否批量.true:将一次性拒绝所有小于deliveryTag的消息。 ③requeue:被拒绝的是否重新入队列
channel.basicNack(delivery.getEnvelope().getDeliveryTag(), false, true);
  • 拒绝单条消息 basicReject ①deliveryTag:发布的每一条消息都会获得一个唯一的deliveryTag,它在channel范围内是唯一的 ②requeue:表示如何处理这条消息,为true表示重新放入RabbitMQ的发送队列中,为false表示通知RabbitMQ销毁该消息 ③channel.basicNack 与 channel.basicReject 的区别在于basicNack可以拒绝多条消息,而basicReject一次只能拒绝一条消息
void basicReject(long deliveryTag, boolean requeue) throws IOException;
  • 拒绝多条消息 basicReject ①deliveryTag:发布的每一条消息都会获得一个唯一的deliveryTag,它在channel范围内是唯一的 ②multiple:批量确认标志,为true表示包含当前消息在内的所有比该消息的deliveryTag值小的消息都被拒绝, 除了已经被应答的消息。为false则表示只拒绝本条消息 ③requeue:表示如何处理这条消息,为true表示重新放入RabbitMQ的发送队列中,为false表示通知RabbitMQ销毁该消息 ④需要注意:如果队列中只有一个消费者的时候,需要确认不会因为拒绝消息并重新放入消息队列中而导致在同一个消费者身上发生死循环。
void basicNack(long deliveryTag, boolean multiple, boolean requeue) throws IOException;
  • 重新投递 basicRecover ①重新投递并没有所谓的像basicReject中的basicReject的deliveryTag参数,所以重新投递好像是将消费者还没有处理的所有的消息都重新放入到队列中,而不是将某一条消息放入到队列中,与basicReject不同的是,重新投递可以指定投递的消息是否允许当前消费者消费。
Basic.RecoverOk basicRecover(boolean requeue);
  • 是否重复投递 ①true :重复投递 ②false: 首次投递
envelope.isRedeliver()

队列Queue:

  • 定义i队列 queueDeclare ①durable:true、false true:在服务器重启时,能够存活 ②exclusive :是否为当前连接的专用队列,在连接断开后,会自动删除该队列,生产环境中应该很少用到吧。 ③autodelete:当没有任何消费者使用时,自动删除该队列。this means that the queue will be deleted when there are no more processes consuming messages from it.
channel.queueDeclare(QUEUE_NAME, false, false, false, null);

通道Channel相关:

  • 绑定队列 ①Queue.DeclareOk queueDeclare(String queue, boolean durable, boolean exclusive, boolean autoDelete,Map<String, Object> arguments) throws IOException;
channel.queueDeclare("Queue", true, false, false, null);
  • 删除队列
Queue.DeleteOk queueDelete(String queue) throws IOException;
  • 删除队列 ①ifUnused 如果为true,则只有在通道没有在使用才被删除。 ②ifEmpty 如果为true,则只有在通道为空才被删除。
Queue.DeleteOk queueDelete(String queue, boolean ifUnused, boolean ifEmpty) throws IOException;
  • 清空队列
Queue.PurgeOk queuePurge(String queue) throws IOException;
  • 发布消息 ①exchange 交换机,可为null ②routingKey 路由密钥,如队列名称 ③props 属性 ,可为null ④body 消息主体
void basicPublish(String exchange, String routingKey, BasicProperties props, byte[] body) throws IOException;

如下:
channel.queueDeclare(queueName, true, false, false, null);
  • 清空队列
Queue.PurgeOk queuePurge(String queue) throws IOException;
  • basicCancel ①同时对于Consumer来说重写handleDelivery方法也是十分方便的。更复杂的Consumer会重写(override)更多的方法,比如handleShutdownSignal当channels和connections close的时候会调用,handleConsumeOk在其他callback方法之前调用,返回consumer tags. ②Consumer同样可以override handleCancelOk和handleCancel方法,这样在显示的或者隐式的取消的时候调用。 ③你可以通过Channel.basicCancel方法显示的cancel一个指定的Consumer ④注意: <1>这句代码首先触发handleConsumerOk,之后触发handleDelivery方法,最后触发handleCancelOk方法。) <2>单个Consumer在Connection上都分配单个的线程来调用这些callback的方法,也就是说Consumer这里安全的调用阻塞式的方法,比如queueDeclare, txCommit, basicCancel或者basicPublish。
channel.basicCancel(consumerTag);

发送消息确认:

  • 单次确认:waitForConfirms。 ①可以看到上面生产者通过Confirm.Select将当前Channel信道设置成confirm模式,broker代理服务器收到之后回传Confirm.Select-Ok同一将当前Channel设置成confirm模式,此外看到返回5条Basic.Ack消息;
 Channel channel = connection.createChannel();
        channel.queueDeclare(queueName, true, false, false, null);
        //将当前信道设置成了confirm模式
        channel.confirmSelect();

        channel.basicPublish("", queueName, null, message.getBytes());

        try {
            //等待broker服务端返回ack或者nack消息
            boolean success = channel.waitForConfirms();
            Log.d(TAG, "队列-" + queueName + "-发送消息:" + message+"  状态:"+success);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
  • 批量确认:addConfirmListener
//开启确认模式
channel.confirmSelect();
channel.addConfirmListener(new ConfirmListener() {
            //消息失败处理
            @Override
            public void handleNack(long deliveryTag, boolean multiple) throws IOException {
                //deliveryTag;唯一消息标签
                //multiple:是否批量
                System.err.println("-------no ack!-----------");
            }
            //消息成功处理
            @Override
            public void handleAck(long deliveryTag, boolean multiple) throws IOException {
                System.err.println("-------ack!-----------");
            }
        });