当前位置: 首页 > news >正文

RabbitMQ实现六类工作模式

😊 @ 作者: 一恍过去
💖 @ 主页: https://blog.csdn.net/zhuocailing3390
🎊 @ 社区: Java技术栈交流
🎉 @ 主题: RabbitMQ实现六类工作模式
⏱️ @ 创作时间: 2023年07月20日

在这里插入图片描述

目录

  • 1、概述
  • 2、工具类封装
  • 3、简单列队模式
  • 4、工作列队模式
  • 5、发布/订阅模式
  • 6、路由模式
  • 7、主题模式
  • 8、交换机说明
  • 9、发布确认模式(消息确认机制)
    • 9.1、消费者实现
    • 9.2、单个确认发布(生产者实现)
    • 9.3、批量确认发布(生产者实现)
    • 9.4、异步确认发布(生产者实现)
    • 9.5、三类确认发布区别对比
  • 10、源码地址

1、概述

六模式:简单模式、工作模式、发布订阅模式、路由模式、主题模式、发布确认模式
参考代码:https://gitee.com/lhzlx/rabbit-simple-demo.git
在这里插入图片描述

2、工具类封装

在下面每种模式的笔记中,会进行代码演示,为了方便,进行工具类的封装,代码如下:

public class RabbitMqUtils {/*** 得到一个连接的  channel** @return* @throws Exception*/public static Channel getChannel() throws Exception {//创建一个连接工厂ConnectionFactory factory = new ConnectionFactory();factory.setHost("127.0.0.1");factory.setUsername("admin");factory.setPassword("admin");Connection connection = factory.newConnection();return connection.createChannel();}
}

3、简单列队模式

生产者通过直接将消息发送到消费者
在这里插入图片描述

代码包路径: lhz.simple

public class Producer {/*** 设置队列名称*/private final static String QUEUE_NAME = "hello";public static void main(String[] args) throws Exception {// 获取ChannelChannel channel = RabbitMqUtils.getChannel();/** 生成一个队列* 1.队列名称* 2.队列里面的消息是否持久化   默认消息存储在内存中* 3.该队列是否只供一个消费者进行消费   是否进行共享   true可以多个消费者消费* 4.是否自动删除   最后一个消费者端开连接以后   该队列是否自动删除   true 自动删除* 5.其他参数*/channel.queueDeclare(QUEUE_NAME, false, false, false, null);String message = "hello world";/** 发送一个消息* 1.发送到那个交换机(可以没有)* 2.路由的  key是哪个* 3.其他的参数信息* 4.发送消息的消息体*/channel.basicPublish("", QUEUE_NAME, null, message.getBytes());System.out.println("消息发送完毕");}
}

消费者:

public class Consumer {/*** 设置队列名称*/private final static String QUEUE_NAME = "hello";public static void main(String[] args) throws Exception {// 获取ChannelChannel channel = RabbitMqUtils.getChannel();// 消费队列消息的一个回调接口DeliverCallback deliverCallback = (consumerTag, delivery) -> {String message = new String(delivery.getBody());System.out.println("消息消费成功,内容:");System.out.println(message);};// 取消消费的一个回调接口   如在消费的时候队列被删除掉了CancelCallback cancelCallback = (consumerTag) -> {System.out.println("消息消费被中断");};/** 消费者消费消息* 1.消费哪个队列* 2.消费成功之后是否要自动应答   true代表自动应答   false手动应答* 3.消费者未成功消费的回调* 3.消费者取消消费的的回调*/channel.basicConsume(QUEUE_NAME, true, deliverCallback, cancelCallback);System.out.println("等待接收消息....");}
}

4、工作列队模式

工作队列: 用来将耗时的任务分发给多个消费者,并且一个消息只能被消费一次,默认情况下RabbitMQ 将按顺序将每条消息发送给下一个消费者==(即轮询分发)==。

主要解决问题: 处理资源密集型任务,并且还要等他完成。有了工作队列,我们就可以将具体的工作放到后面去做,将工作封装为一个消息,发送到队列中,一个工作进程就可以取出消息并完成工作。如果启动了多个工作进程,那么工作就可以在多个进程间共享。
在这里插入图片描述

代码包路径: lhz.work

public class Producer {/*** 设置队列名称*/private final static String QUEUE_NAME = "hello";public static void main(String[] args) throws Exception {try (Channel channel = RabbitMqUtils.getChannel();) {channel.queueDeclare(QUEUE_NAME, false, false, false, null);// 从控制台当中接受信息Scanner scanner = new Scanner(System.in);while (scanner.hasNext()) {String message = scanner.next();channel.basicPublish("", QUEUE_NAME, null, message.getBytes());System.out.println("发送消息完成:" + message);}}}
}

消费者:
消费者需要两个,分别为:Consumer01 Consumer02两者代码一致,只是名称不同,下面以Consumer01 代码为例:

// Consumer01与Consumer02代码一致
public class Consumer01 {/*** 设置队列名称*/private final static String QUEUE_NAME = "hello";public static void main(String[] args) throws Exception {// 获取ChannelChannel channel = RabbitMqUtils.getChannel();// 消费队列消息的一个回调接口DeliverCallback deliverCallback = (consumerTag, delivery) -> {String message = new String(delivery.getBody());System.out.println("Consumer01消息消费成功,内容:");System.out.println(message);};// 取消消费的一个回调接口   如在消费的时候队列被删除掉了CancelCallback cancelCallback = (consumerTag) -> {System.out.println("Consumer01消息消费被中断");};//采用自动应答channel.basicConsume(QUEUE_NAME, true, deliverCallback, cancelCallback);System.out.println("Consumer01等待接收消息....");}
}

结果:
为了演示工作队列的轮询分发,需要启动两个Consumer实例,然后再启动Producer并且在控制台多次输入内容,可以看到两个Consumer会依次接收消息,结果如下:
在这里插入图片描述

5、发布/订阅模式

发布/订阅模式是:
生产者将消息发送到交换机中,由交换机发送给不同类型的消费者,做到发布一次,消费多个,如果消费者绑定的队列名称一样,将按照轮询进行消费,所以保证了:同一个队列的中的消息不会被重复消费;

比如:
它包含一个生产者、多个消费者、两个队列和一个交换机。两个消费者同时绑定到不同的队列上去,两个队列绑定到交换机上去,生产者通过发送消息到交换机,所有消费者接收并消费消息。

在这里插入图片描述

代码包路径: lhz.fanout

public class Producer {/*** 定义交换机和队列名称*/private static final String EXCHANGE_NAME = "fanout_exchange";public static void main(String[] args) throws Exception {// 获取ChannelChannel channel = RabbitMqUtils.getChannel();/*绑定的交换机 参数1交互机名称 参数2 exchange类型 */channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.FANOUT);for (int i = 0; i < 10; i++) {String message = "消息:" + i;// 发送一个消息,1.发送到那个交换机,2.路由的  key是哪个,3.其他的参数信息,4.发送消息的消息体channel.basicPublish(EXCHANGE_NAME, "", null, message.getBytes());}System.out.println("消息发送完毕");}
}

消费者:
消费者需要两个,分别为:Consumer01 Consumer02两者代码一致,只是名称不同,下面以Consumer01 代码为例:

/**
*两个消费者逻辑代码一样,只是绑定的队列不同,Consumer01:consumerFanout_sms;Consumer02:consumerFanout_email
*/
public class Consumer01(02) {/*** 设置队列及交换机名称*/private static final String QUEUE_NAME = "consumerFanout_sms";private static final String QUEUE_NAME = "consumerFanout_email";private static final String EXCHANGE_NAME = "fanout_exchange";public static void main(String[] args) throws Exception {// 获取ChannelChannel channel = RabbitMqUtils.getChannel();//消费者关联队列channel.queueDeclare(QUEUE_NAME, false, false, false, null);// 消费者绑定交换机 参数1 队列 参数2 交换机 参数3 routingKeychannel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "");// 消费队列消息的一个回调接口DeliverCallback deliverCallback = (consumerTag, delivery) -> {String message = new String(delivery.getBody());System.out.println("Consumer01(02)消息消费成功,内容:");System.out.println(message);};// 取消消费的一个回调接口   如在消费的时候队列被删除掉了CancelCallback cancelCallback = (consumerTag) -> {System.out.println("Consumer01(02)消息消费被中断");};channel.basicConsume(QUEUE_NAME, true, deliverCallback, cancelCallback);System.out.println("Consumer01(02)等待接收消息....");}
}

结果:
先启动一次生产者,再启动两个消费者,绑定到交换机A,以及两个不同的队列、然后再重新启动生产者,绑定到交换机A;

在生产者发生消息以后,两个不同名称的消费者队列都可以接收到消息相同的内容,这些因为不同的消费者绑定了同一个交换机
注意:如果消费者绑定的队列名称一样,将按照轮询进行消费,所以保证了:同一个队列的中的消息不会被重复消费;**

6、路由模式

路由模式:
跟发布订阅模式类似,在订阅模式的基础上修改了exchange类型以及加上了路由键,如果消费者的路由键一样,其效果和发布/订阅模式一致订阅模式是分发到所有绑定到交换机的所有队列,路由模式只分发到绑定在交换机上面指定路由键的队列一个队列可以绑定多个不同的路由

注意: 消息可能匹配多个消费者,但是同一个队列的中的消息不会被重复消费;

我们可以看一下下面这张图:

在这里插入图片描述
在上面这张图中,我们可以看到 X 绑定了两个队列,绑定类型是 direct。队列 Q1 绑定键为 orange, 队列 Q2 绑定键有两个:一个绑定键为 black,另一个绑定键为 green。
在这种绑定情况下,生产者发布消息到 exchange 上,绑定键为 orange 的消息会被发布到队列 Q1。绑定键为 black、green的消息会被发布到队列 Q2,其他消息类型的消息将被丢弃。

说明: 生产者发送消息到交换机,同时定义了一个路由 routingKey,多个消费者声明多个队列,与交换机进行绑定,同时定义路由 routingKey,只有和生产者发送消息时的路由 routingKey相同的消费者才能消费数据

注意: 如果交换机和路由绑定后,需要修改路由就要修改交换机名称

代码包路径:lhz.route

生产者:

public class Producer {/*** 定义交换机和队列名称*/private static final String EXCHANGE_NAME = "direct_exchange";public static void main(String[] args) throws Exception {// 获取ChannelChannel channel = RabbitMqUtils.getChannel();/*绑定的交换机 参数1交互机名称 参数2 exchange类型 */channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);// 发送消息String message = "", sendType = "";for (int i = 0; i < 20; i++) {if (i % 2 == 0) {sendType = "info";message = "我是 info 级别的消息类型:" + i;} else {sendType = "error";message = "我是 error 级别的消息类型:" + i;}System.out.println("[send]:" + message + "  " + sendType);// 第二个参数就是路由键channel.basicPublish(EXCHANGE_NAME, sendType, null, message.getBytes());}System.out.println("消息发送完毕");}
}

消费者:
消费者需要两个,分别为:Consumer01 Consumer02两者代码一致,只是名称不同,下面以Consumer01 代码为例:

/**
* 两个消费者逻辑代码一样,只是绑定的队列不同和不同的路由键
* Consumer01:"info"、"consumer_info";Consumer02:"error"、"consumer_error";
*/
public class Consumer01(02) {/*** 设置队列及交换机名称*/private static final String ROUTING_KEY = "info";private static final String ROUTING_KEY = "error";private static final String QUEUE_NAME = "consumer_info";private static final String QUEUE_NAME = "consumer_error";private static final String EXCHANGE_NAME = "direct_exchange";public static void main(String[] args) throws Exception {// 获取ChannelChannel channel = RabbitMqUtils.getChannel();//消费者关联队列channel.queueDeclare(QUEUE_NAME, false, false, false, null);// 消费者绑定交换机 参数1 队列 参数2 交换机 参数3 routingKeychannel.queueBind(QUEUE_NAME, EXCHANGE_NAME, ROUTING_KEY);// 消费队列消息的一个回调接口DeliverCallback deliverCallback = (consumerTag, delivery) -> {String message = new String(delivery.getBody());System.out.println("Consumer01(02)消息消费成功,内容:");System.out.println(message);};// 取消消费的一个回调接口   如在消费的时候队列被删除掉了CancelCallback cancelCallback = (consumerTag) -> {System.out.println("Consumer01(02)消息消费被中断");};channel.basicConsume(QUEUE_NAME, true, deliverCallback, cancelCallback);System.out.println("Consumer01(02)等待接收消息....");}
}

结果:
先启动一次生产者,再启动两个消费者,绑定到交换机A,以及两个不同的队列和不同的路由键;最后重新启动生产者,绑定到交换机A;

7、主题模式

主题模式:
routing 路由模式类似,只不过路由模式是指定固定的路由键 routingKey,而主题模式是可以模糊匹配路由routingKey,类似于SQL中 = 和 like 的关系

注意:消息可能匹配多个消费者,但是同一个队列的中的消息不会被重复消费;
要求:

  • 要求
    Topic 模式消息的 routing_key 不能随意写,必须满足一定的要求,它必须是一个单词列表,以 “.” 或者 “#” 分隔开。这些单词可以是任意单词,这个单词列表最多不能超过 255 个字节。

  • 分隔符

    “*(星号)”:可以代替一个单词

    “#(井号)”:可以替代零个或多个单词

  • 比如

    • 中间带 orange 带3个单词: *.orange.*
    • 最后一个词是 rabbit 的3 个单词:*.*.rabbit
    • 以 lazy开头的多个单词lazy.#

图示:
在这里插入图片描述

注意: 如果交换机和路由绑定后,需要修改路由就要修改交换机名称

代码包路径:lhz.toptic

生产者:

public class Producer {/*** 定义交换机和队列名称*/private static final String EXCHANGE_NAME = "topic";public static void main(String[] args) throws Exception {// 获取ChannelChannel channel = RabbitMqUtils.getChannel();/*绑定的交换机 参数1交互机名称 参数2 exchange类型 */channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.TOPIC);// 定义路由keyString routingKey = "mq.info.log";String message = "topic_exchange_msg:" + routingKey;System.out.println("[send] = " + message);// 发送消息// 第二个参数就是路由键channel.basicPublish(EXCHANGE_NAME, routingKey, null, message.getBytes());System.out.println("消息发送完毕");}
}

消费者:

public class Consumer {/*** 设置路由匹配规则*/private static final String ROUTING_KEY = "#.log";/*** 设置队列及交换机名称*/private static final String QUEUE_NAME = "topic_consumer";private static final String EXCHANGE_NAME = "topic";public static void main(String[] args) throws Exception {// 获取ChannelChannel channel = RabbitMqUtils.getChannel();//消费者关联队列channel.queueDeclare(QUEUE_NAME, false, false, false, null);// 消费者绑定交换机 参数1 队列 参数2 交换机 参数3 routingKeychannel.queueBind(QUEUE_NAME, EXCHANGE_NAME, ROUTING_KEY);// 消费队列消息的一个回调接口DeliverCallback deliverCallback = (consumerTag, delivery) -> {String message = new String(delivery.getBody());System.out.println("消息消费成功,内容:");System.out.println(message);};// 取消消费的一个回调接口   如在消费的时候队列被删除掉了CancelCallback cancelCallback = (consumerTag) -> {System.out.println("消息消费被中断");};channel.basicConsume(QUEUE_NAME, true, deliverCallback, cancelCallback);System.out.println("等待接收消息....");}
}

结果:
先启动消费者,绑定到交换机A,通过通配符模糊匹配路由;再启动生产者,绑定到交换机A,设置具体的路由键;

8、交换机说明

在没有交换机的情况,生产者直接往队列发送消息,消费者绑定队列消费相消息,但是同一个队列中一个消息只会被消费一次,所以无法满足一个消息同时被多个消费者使用;

​交换机的作用就可以解决这个问题,一个交换机可以绑定多个不同的队列,一个队列绑定多个消费者;生产者将消息发送到交换机中,所有绑定了该交换机的队列都可以收到消息;所以生产者发送一次消息,可以被不同的队列**(消费者)**进行消费;当同一个队列中存在多个消费者时,消息不会被重复消费;

9、发布确认模式(消息确认机制)

概念: 生产者将信道设置成 确认(confirm) 模式,一旦信道进入 confirm 模式,所有在该信道上面发布的消息都将会被指派一个唯一的 ID(从 1 开始),一旦消息被投递到所有匹配的队列之后,broker就会发送一个确认给生产者(包含消息的唯一 ID),这就使得生产者知道消息已经正确到达目的队列了,如果消息和队列是可持久化的,那么确认消息会在将消息写入磁盘之后发出,broker 回传给生产者的确认消息中 delivery-tag 域包含了确认消息的序列号。

优点: confirm 模式最大的好处在于他是异步的,一旦发布一条消息,生产者就可以在等待信道返回确认的同时继续发送下一条消息,当消息最终得到ack之后,生产者可以通过回调方法来处理该ack消息;如果 RabbitMQ 因为自身内部错误导致消息丢失,就会发送一条 nack 消息,生产者应用程序同样可以在回调方法中处理该 nack 消息;

使用: 发布确认模式默认是没有开启,生产者通过调用方法 confirmSelect实现开启
在这里插入图片描述

9.1、消费者实现

代码包路径: lhz.confirm

public class Consumer {//设置队列名称private final static String QUEUE_NAME = "confirm_queue";public static void main(String[] args) throws Exception {// 获取ChannelChannel channel = RabbitMqUtils.getChannel();// 消费队列消息的一个回调接口DeliverCallback deliverCallback = (consumerTag, delivery) -> {String message = new String(delivery.getBody());System.out.println("消息消费成功,内容:");System.out.println(message);};// 取消消费的一个回调接口   如在消费的时候队列被删除掉了CancelCallback cancelCallback = (consumerTag) -> {System.out.println("消息消费被中断");};// 消费者消费消息channel.basicConsume(QUEUE_NAME, true, deliverCallback, cancelCallback);System.out.println("等待接收消息....");}
}

9.2、单个确认发布(生产者实现)

单个确认发布:
是一种简单的确认方式,它是一种**同步确认发布** 的方式,也就是发布的消息只有被确认发布之后,后续的消息才能继续发布,通过waitForConfirmsOrDie(long outTime)方法,指定时间范围内(单位:毫秒)这个消息没有被确认那么它将抛出异常;通过waitForConfirms()对broker响应的消息进行确认;

这种确认方式有一个最大的缺点就是:发布速度特别的慢,因为如果没有确认发布的消息就会阻塞所有后续消息的发布,这种方式最多提供每秒不超过数百条发布消息的吞吐量,当然对于某 些应用程序来说这可能已经足够了。

代码包路径: lhz.confirm
类:SingleProducer
说明: 确认发布的实现只是对生产者代码做修改,所以消费者代码不变,参考:《9.1、消费者实现》
步骤: 启动消费者,发送消息,然后观察耗时即可;

public class SingleProducer {// 设置队列名称private final static String QUEUE_NAME = "confirm_queue";// 发送消息数量private final static Integer MESSAGE_COUNT = 100;public static void main(String[] args) throws Exception {try (Channel channel = RabbitMqUtils.getChannel()) {channel.queueDeclare(QUEUE_NAME, false, false, false, null);//开启发布确认channel.confirmSelect();long begin = System.currentTimeMillis();for (int i = 0; i < MESSAGE_COUNT; i++) {String message = i + "";channel.basicPublish("", QUEUE_NAME, null, message.getBytes());//服务端返回确认状态,如果 false或超时时间内未返回,生产者可以消息重发boolean flag = channel.waitForConfirms();if (flag) {System.out.println("消息发送成功");}}long end = System.currentTimeMillis();System.out.println("发布" + MESSAGE_COUNT + "个单独确认消息,耗时" + (end - begin) + "ms");}}
}

9.3、批量确认发布(生产者实现)

批量确认发布:
与单个等待确认消息相比,会先发布一批消息然后一起确认 可以极大地提高吞吐量,它也是一种**同步确认发布** 的方式。

这种方式的缺点就是: 当消息发布出现问题时,不知道是哪个消息出现问题了;

代码包路径:lhz.confirm
类:BatchProducer
**说明:**确认发布的实现只是对生产者代码做修改,所以消费者代码不变,参考:《9.1、消费者实现》
步骤: 启动消费者,发送消息,然后观察耗时即可;

public class BatchProducer {// 设置队列名称private final static String QUEUE_NAME = "confirm_queue";// 发送消息数量private final static Integer MESSAGE_COUNT = 100;public static void main(String[] args) throws Exception {try (Channel channel = RabbitMqUtils.getChannel()) {channel.queueDeclare(QUEUE_NAME, false, false, false, null);// 开启发布确认channel.confirmSelect();// 批量确认消息大小int batchSize = 100;// 未确认消息个数int outstandingMessageCount = 0;long begin = System.currentTimeMillis();for (int i = 0; i < MESSAGE_COUNT; i++) {String message = i + "";channel.basicPublish("", QUEUE_NAME, null, message.getBytes());outstandingMessageCount++;// 达到设置的批处理大小时,进行确认if (outstandingMessageCount == batchSize) {channel.waitForConfirms();outstandingMessageCount = 0;}}// 为了确保还有剩余没有确认消息,进行再次确认if (outstandingMessageCount > 0) {channel.waitForConfirms();}long end = System.currentTimeMillis();System.out.println("发布" + MESSAGE_COUNT + "个批量确认消息,耗时" + (end - begin) + "ms");}}
}

9.4、异步确认发布(生产者实现)

异步确认发布:
虽然编程逻辑比上两个要复杂,但是可靠性和效率更高;他是利用回调函数来保证是否投递成功。逻辑图如下:
在这里插入图片描述

代码包路径:lhz.confirm
类:AsynProducer
**说明:**确认发布的实现只是对生产者代码做修改,所以消费者代码不变,参考:《9.1、消费者实现》
步骤: 启动消费者,发送消息,然后观察耗时即可;

public class AsynProducer {/*** 设置队列名称*/private final static String QUEUE_NAME = "confirm_queue";/*** 发送消息数量*/private final static Integer MESSAGE_COUNT = 500;public static void main(String[] args) throws Exception {try (Channel channel = RabbitMqUtils.getChannel()) {channel.queueDeclare(QUEUE_NAME, false, false, false, null);//开启发布确认channel.confirmSelect();/** 线程安全有序的一个哈希表,适用于高并发的情况* 1.轻松的将序号与消息进行关联* 2.轻松批量删除条目    只要给到序列号* 3.支持并发访问*/ConcurrentSkipListMap<Long, String> outstandingConfirms = new ConcurrentSkipListMap();/** 确认收到消息的一个回调* 1.消息序列号* 2.true可以确认小于等于当前序列号的消息*   false确认当前序列号消息*/ConfirmCallback ackCallback = (sequenceNumber, multiple) -> {if (multiple) {//返回的是小于等于当前序列号的未确认消息    是一个  mapConcurrentNavigableMap<Long, String> confirmed = outstandingConfirms.headMap(sequenceNumber, true);//清除该部分未确认消息confirmed.clear();} else {//只清除当前序列号的消息outstandingConfirms.remove(sequenceNumber);}};// 未被确认消息回调ConfirmCallback nackCallback = (sequenceNumber, multiple) -> {String message = outstandingConfirms.get(sequenceNumber);System.out.println("未被确认消息:" + message + ",序列号" + sequenceNumber);};/** 添加一个异步确认的监听器* 1.确认收到消息的回调* 2.未收到消息的回调*/channel.addConfirmListener(ackCallback, nackCallback);long begin = System.currentTimeMillis();for (int i = 0; i < MESSAGE_COUNT; i++) {String message = "消息" + i;/** channel.getNextPublishSeqNo()获取下一个消息的序列号* 通过序列号与消息体进行一个关联* 全部都是未确认的消息体*/outstandingConfirms.put(channel.getNextPublishSeqNo(), message);channel.basicPublish("", QUEUE_NAME, null, message.getBytes());}long end = System.currentTimeMillis();System.out.println("发布" + MESSAGE_COUNT + "个异步确认消息,耗时" + (end - begin) + "ms");}}
}

9.5、三类确认发布区别对比

  • 单独发布消息
    同步等待确认,简单,但吞吐量非常有限,较耗时。

  • 批量发布消息
    批量同步等待确认,简单,合理的吞吐量,一旦出现问题但很难推断出是那条 消息出现了问题,效率较高。

  • 异步处理:
    最佳性能和资源使用,在出现错误的情况下可以很好地控制,但是实现起来稍微难些,效率高。

10、源码地址

源码地址:https://gitee.com/lhzlx/rabbit-simple-demo.git

在这里插入图片描述

相关文章:

RabbitMQ实现六类工作模式

&#x1f60a; 作者&#xff1a; 一恍过去 &#x1f496; 主页&#xff1a; https://blog.csdn.net/zhuocailing3390 &#x1f38a; 社区&#xff1a; Java技术栈交流 &#x1f389; 主题&#xff1a; RabbitMQ实现六类工作模式 ⏱️ 创作时间&#xff1a; 2023年07月20日…...

all in one (群辉、软路由、win/linux)折腾日记

目录 生命不息&#xff0c;折腾不止名词解释硬件参数装机 生命不息&#xff0c;折腾不止 因自身能力有限&#xff0c;可能内容质量不高&#xff0c;欢迎志同道合的各路大神加入&#xff0c;共同折腾&#xff01; 名词解释 ALL IN ONE &#xff1a;多功能一体机 OpenWrt【软路…...

【Redis】2、Redis应用之【根据 Session 和 Redis 进行登录校验和发送短信验证码】

目录 一、基于 Session 实现登录(1) 发送短信验证码① 手机号格式后端校验② 生成短信验证码 (2) 短信验证码登录、注册(3) 登录验证① 通过 SpringMVC 定义拦截器② ThreadLocal (4) 集群 Session 不共享问题 二、基于 Redis 实现共享 session 登录(1) 登录之后&#xff0c;缓…...

MiniGPT4系列之二推理篇命令行方式:在RTX-3090 Ubuntu服务器推理详解

MiniGPT4系列之一部署篇&#xff1a;在RTX-3090 Ubuntu服务器部署步骤详解_seaside2003的博客-CSDN博客 MiniGPT4系列之二推理篇命令行方式&#xff1a;在RTX-3090 Ubuntu服务器推理详解_seaside2003的博客-CSDN博客 MiniGPT4系列之三模型推理 (Web UI)&#xff1a;在RTX-309…...

Android TvSettings Bug: 密码框无法点击唤起输入法

概述 Android 10 的Box方案&#xff0c; 默认使用的是TvSettings作为系统设置&#xff0c;输入操作的习惯上是使用鼠标&#xff0c;键盘&#xff0c;遥控&#xff0c;日常的场景是没有问题&#xff0c;也不会出现本文中提及的问题。当外接的USB触摸屏后&#xff0c;出现无法点击…...

Windows, MacOS还是Linux好?

今天我们来聊一个小话题&#xff1a;选操作系统&#xff0c;是哪个好&#xff1f;今天&#xff0c;我以一个介绍者的身份给大家推荐&#xff0c;我就不出什么点子了。 Windows Windows&#xff0c;是一个老牌的操作系统。他的优处和短处和有很多&#xff0c;我们来介绍一下 优…...

Gateway自定义过滤器——全局过滤器

一、什么是全局过滤器&#x1f349; 首先&#xff0c;我们要知道全局过滤器其实是特殊路由过滤器(特殊的GatewayFilter)&#xff0c;会有条件地作用于所有路由。 为什么要自定义全局过滤器&#xff1f;就好比是看大门的保安大叔&#xff0c;平时主要是做好进出大门外来人员登记…...

Android App的几个核心概念

Application启动 点击桌面图标启动App(如下流程图) 针对以上流程图示&#xff1a; ActivityManagerService#startProcessLocked()Process#start()ActivityThread#main()&#xff0c;入口分析的地方ActivityThread#attach()&#xff0c;这个里面的逻辑很核心 ActivityManagerS…...

window10安装telnet

1、打开控制面板 2、点击程序和功能 3、点击启用或关闭Windows功能 4、选中Telnet客户端&#xff0c;然后点击确定&#xff0c;然后就可以使用telnent 主机 端口来查看本地是否能连通该主机的该端口。...

大厂sql真题讲解(黑马)

2023 http://yun.itheima.com/open/853.html | 面试宝典-如何备战大厂SQL真题 http://yun.itheima.com/open/858.html | 面试宝典–大厂必考知识开窗函数 http://yun.itheima.com/open/864.html | 面试宝典-详解美团SQL真题 http://yun.itheima.com/open/868.html | 图解大…...

SpringBoot整合EasyExcel实现读操作

&#x1f60a; 作者&#xff1a; 一恍过去 &#x1f496; 主页&#xff1a; https://blog.csdn.net/zhuocailing3390 &#x1f38a; 社区&#xff1a; Java技术栈交流 &#x1f389; 主题&#xff1a; SpringBoot整合EasyExcel实现读操作 ⏱️ 创作时间&#xff1a; 2023年…...

go mod 设置国内源 windows 环境 win10

启用 go module 功能 go env -w GO111MODULEon 配置 goproxy 变量 go env -w GOPROXYhttps://goproxy.cn,direct 下载包就行了&#xff0c;速度飞快 go mod tidy 检测 goproxy 是否配置好 运行 go env | findstr goproxy 查看 goproxy Go module 从 Go v1.12 版本开始存在&a…...

智能决策支持系统实现的关键技术分析

1.模 型 中 的 关 键 因 素 。 在 按 本 模 型 研 究 开 发 系 统 时 ,应 当 着 重 考 虑 以 下 几 个 因 素 : (1)设 备 保 护 需 求 计 划 。 保 护 需 求 包 括 了 人 员 、 物 质 、 财 务 等 各项 因 素 ; (2)考 虑 设 备 运行 及维修的 历史数据。 这是进 行 模 型 选 择…...

OSS对象存储后端实现+Vue实现图片上传【基于若依管理系统开发】

文章目录 基本介绍术语介绍图片上传方式介绍普通上传用户直传应用服务器签名后直传 OSS对象存储后端实现maven配置文件配置类ServiceController 图片上传前端图片上传组件api页面使用组件组件效果 基本介绍 术语介绍 Bucket&#xff08;存储空间&#xff09;&#xff1a;用于…...

人工智能学习目录

1、人工智能-电脑如何像人一样思考&#xff1f; 从发展历史到人工智能的应用案例&#xff0c;再到人工智能本质是数学问题&#xff0c;从房价预测问题提出损失函数由参数导致&#xff0c;再由损失函数的最优值入手引入梯度下降法&#xff0c;最后到多参数方程的最优求解。 人工…...

Vue单页面实现el-tree el-breadcrumb功能、el-tree右键点击树节点展示菜单功能、树节点编辑节点字段名称功能

(1) 点击el-tree节点 使用el-breadcrumb展示选中树节点及父项数据 重点&#xff1a;handleNodeClick方法、getTreeNode方法 (2) 选择el-breadcrumb-item设置el-tree节点选中 必须设置属性: current-node-key"currentNodeKey" 、 node-key"id" 重点: 设置…...

C++核心编程之函数高级使用

目录 一、函数的默认参数 二、函数占位参数 三、函数重载 四、函数重载-注意事项 一、函数的默认参数 在C中&#xff0c;函数的形参列表中的形参是可以有默认值的 语法&#xff1a;返回值类型 函数名 &#xff08;参数默认值&#xff09;{} 示例1&#xff1a; #includ…...

如何创建智能合约游戏系统

区块技术的发展&#xff0c;智能合约成为了一个热门话题。智能合约是一种基于区块技术的自动化合约&#xff0c;它可以自动执行合同中规定的条款&#xff0c;从而实现去中心化的信任和价值传递。在游戏领域&#xff0c;智能合约可以让玩家在游戏中实现各种交易和交互&#xff0…...

如何用rust实现一个异步channel

目录 前言思路实现功能代码实现 测试先引测试版包测试代码结果与分析思考 尾语 前言 使用通信来共享内存&#xff0c;而不是通过共享内存来通信 上面这句话&#xff0c;是每个go开发者在 处理多线程通信时 的座右铭&#xff0c;go甚至把实现这个理念的channel直接焊在编译器里&…...

gitee上传项目到仓库

目录 一些常用的Git命令切换到其他盘符&#xff1a;列出目录下的所有文件&#xff1a;以树状图的形式显示目录下的文件和子目录:返回上一层目录&#xff1a;写的C#代码文件上传到新建的Git仓库中&#xff0c;可以按照以下步骤进行操作&#xff1a;出现的错误&#xff1a; 一些常…...

【element-ui】el-autocomplete实现 无数据匹配

文章目录 方法一&#xff1a;使用 default 插槽方法二&#xff1a;使用 empty-text 属性&#xff08;适用于列表类型&#xff09;总结 在使用 Element UI 的 el-autocomplete 组件时&#xff0c;如果你希望在没有任何数据匹配的情况下显示特定的内容&#xff0c;你可以通过自定…...

Playwright自动化测试全栈指南:从基础到企业级实践(2025终极版)

引言 在Web应用复杂度指数级增长的今天,传统自动化测试工具面临​​动态渲染适配难​​、​​多浏览器兼容差​​、​​测试稳定性低​​三大挑战。微软开源的Playwright凭借​​跨浏览器支持​​、​​自动等待机制​​和​​原生异步架构​​,成为新一代自动化测试的事实标…...

移除元素-JavaScript【算法学习day.04】

题目链接&#xff1a;27. 移除元素 - 力扣&#xff08;LeetCode&#xff09; 第一种思路 标签&#xff1a;拷贝覆盖 主要思路是遍历数组 nums&#xff0c;每次取出的数字变量为 num&#xff0c;同时设置一个下标 ans 在遍历过程中如果出现数字与需要移除的值不相同时&#xff…...

【LLM】多智能体系统 Why Do Multi-Agent LLM Systems Fail?

note 构建一个成功的 MAS&#xff0c;不仅仅是提升底层 LLM 的智能那么简单&#xff0c;它更像是在构建一个组织。如果组织结构、沟通协议、权责分配、质量控制流程设计不当&#xff0c;即使每个成员&#xff08;智能体&#xff09;都很“聪明”&#xff0c;整个系统也可能像一…...

一个完整的日志收集方案:Elasticsearch + Logstash + Kibana+Filebeat (二)

&#x1f4c4; 本地 Windows 部署 Logstash 连接本地 Elasticsearch 指南 ✅ 目标 在本地 Windows 上安装并运行 Logstash配置 Logstash 将数据发送至本地 Elasticsearch测试数据采集与 ES 存储流程 &#x1f9f0; 前提条件 软件版本要求安装说明Java17Oracle JDK 下载 或 O…...

Java泛型中的通配符详解

无界通配符 通配符的必要性 通过WrapperUtil类的示例可以清晰展示通配符的使用场景。假设我们需要为Wrapper类创建一个工具类WrapperUtil&#xff0c;其中包含一个静态方法printDetails()&#xff0c;该方法需要处理任意类型的Wrapper对象。最初的实现尝试如下&#xff1a; …...

最新Spring Security实战教程(十七)企业级安全方案设计 - 多因素认证(MFA)实现

&#x1f337; 古之立大事者&#xff0c;不惟有超世之才&#xff0c;亦必有坚忍不拔之志 &#x1f390; 个人CSND主页——Micro麦可乐的博客 &#x1f425;《Docker实操教程》专栏以最新的Centos版本为基础进行Docker实操教程&#xff0c;入门到实战 &#x1f33a;《RabbitMQ》…...

【高效开发工具系列】Blackmagic Disk Speed Test for Mac:专业硬盘测速工具

博客目录 一、Blackmagic Disk Speed Test 概述二、软件核心功能解析三、v3.3 版本的新特性与改进四、实际应用场景分析五、使用技巧与最佳实践六、与其他工具的比较及优势 一、Blackmagic Disk Speed Test 概述 Blackmagic Disk Speed Test 是 Mac 平台上广受专业人士青睐的一…...

QtDBus模块功能及架构解析

Qt 6.0 中的 QtDBus 模块是一个用于进程间通信&#xff08;IPC&#xff09;的核心模块&#xff0c;它基于 D-Bus 协议实现。D-Bus 是一种在 Linux 和其他类 Unix 系统上广泛使用的消息总线系统&#xff0c;允许应用程序和服务相互通信。 一、QtDBus模块主要功能&#xff1a; 1…...

字符串字典序最大后缀问题详解

字符串字典序最大后缀问题详解 一、问题定义与背景1.1 问题描述1.2 实际应用场景 二、暴力解法及其局限性2.1 暴力解法思路2.2 代码示例2.3 局限性分析 三、双指针算法&#xff1a;高效解决方案3.1 算法核心思想3.2 算法步骤3.3 代码实现3.4 与暴力解法对比 四、复杂度分析4.1 …...