赞
踩
应用解耦
大数据的杀手锏,谈到大数据领域内的消息传输,则绕不开 Kafka,这款为大数据而生的消息中间件, 以其百万级 TPS 的吞吐量名声大噪,迅速成为大数据领域的宠儿,在数据采集、传输、存储的过程中发挥 着举足轻重的作用。目前已经被 LinkedIn,Uber, Twitter, Netflix 等大公司所采纳。
优点:性能卓越,单机写入 TPS 约在百万条/秒,最大的优点,就是吞吐量高。时效性 ms 级可用性非 常高,kafka 是分布式的,一个数据多个副本,少数机器宕机,不会丢失数据,不会导致不可用,消费者采 用 Pull 方式获取消息, 消息有序, 通过控制能够保证所有消息被消费且仅被消费一次;有优秀的第三方Kafka Web 管理界面 Kafka-Manager;在日志领域比较成熟,被多家公司和多个开源项目使用;功能支持: 功能 较为简单,主要支持简单的 MQ 功能,在大数据领域的实时计算以及日志采集被大规模使用
缺点:Kafka 单机超过 64 个队列/分区,Load 会发生明显的飙高现象,队列越多,load 越高,发送消 息响应时间变长,使用短轮询方式,实时性取决于轮询间隔时间,消费失败不支持重试;支持消息顺序, 但是一台代理宕机后,就会产生消息乱序,社区更新较慢;
RocketMQ
RocketMQ 出自阿里巴巴的开源产品,用 Java 语言实现,在设计时参考了 Kafka,并做出了自己的一 些改进。被阿里巴巴广泛应用在订单,交易,充值,流计算,消息推送,日志流式处理,binglog 分发等场 景。
优点:单机吞吐量十万级,可用性非常高,分布式架构,消息可以做到 0 丢失,MQ 功能较为完善,还是分 布式的,扩展性好,支持 10 亿级别的消息堆积,不会因为堆积导致性能下降,源码是 java 我们可以自己阅 读源码,定制自己公司的 MQ
缺点:支持的客户端语言不多,目前是 java 及 c++,其中 c++不成熟;社区活跃度一般,没有在MQ 核心中去实现 JMS 等接口,有些系统要迁移需要修改大量代码
RabbitMQ
官网更新:https://www.rabbitmq.com/news.html(opens new window)
2007 年发布,是一个在AMQP(高级消息队列协议)基础上完成的,可复用的企业消息系统,是当前最主流的消息中间件之一。
优点:由于 erlang 语言的高并发特性,性能较好;吞吐量到万级,MQ 功能比较完备,健壮、稳定、易 用、跨平台、支持多种语言 如:Python、Ruby、.NET、Java、JMS、C、PHP、ActionScript、XMPP、STOMP 等,支持 AJAX 文档齐全;开源提供的管理界面非常棒,用起来很好用,社区活跃度高;更新频率相当高
缺点:商业版需要收费,学习成本较高
Kafka :主要特点是基于Pull 的模式来处理消息消费,追求高吞吐量,一开始的目的就是用于日志收集 和传输,适合产生大量数据的互联网服务的数据收集业务。大型公司建议可以选用,如果有日志采集功能, 肯定是首选 kafka 了。
RocketMQ:天生为金融互联网领域而生,对于可靠性要求很高的场景,尤其是电商里面的订单扣款,以及业务削 峰,在大量交易涌入时,后端可能无法及时处理的情况。RoketMQ 在稳定性上可能更值得信赖,这些业务 场景在阿里双 11 已经经历了多次考验,如果你的业务有上述并发场景,建议可以选择 RocketMQ。
RabbitMQ:结合 erlang 语言本身的并发优势,性能好时效性微秒级,社区活跃度也比较高,管理界面用起来十分 方便,如果你的数据量没有那么大,中小型公司优先选择功能比较完备的 RabbitMQ。
生产者
交换机
队列
消费者
Broker
Virtual host
Connection
Channel
Exchange
Queue
Binding
rabbitmq-server-3.8.8-1.el7.noarch.rpm
GitHub:https://github.com/rabbitmq/rabbitmq-server/releases/tag/v3.8.8(opens new window)
加载下载:https://packagecloud.io/rabbitmq/rabbitmq-server/packages/el/7/rabbitmq-server-3.8.8-1.el7.noarch.rpm(opens new window)
erlang-21.3.8.21-1.el7.x86_64.rpm
官网:https://www.erlang-solutions.com/downloads/
加速:https://packagecloud.io/rabbitmq/erlang/packages/el/7/erlang-21.3.8.21-1.el7.x86_64.rpm
rpm -ivh erlang-21.3.8.21-1.el7.x86_64.rpm
yum install socat -y
rpm -ivh rabbitmq-server-3.8.8-1.el7.noarch.rpm
# 启动服务
systemctl start rabbitmq-server
# 查看服务状态
systemctl status rabbitmq-server
# 开机自启动
systemctl enable rabbitmq-server
# 停止服务
systemctl stop rabbitmq-server
# 重启服务
systemctl restart rabbitmq-server
rabbitmq-plugins enable rabbitmq_management
systemctl restart rabbitmq-server
访问 http://42.192.149.71:15672 ,用默认账号密码(guest)登录,出现权限问题
默认情况只能在 localhost 本机下访问,所以需要添加一个远程登录的用户
# 创建账号和密码
rabbitmqctl add_user admin 123456
# 设置用户角色
rabbitmqctl set_user_tags admin administrator
# 为用户添加资源权限
# set_permissions [-p <vhostpath>] <user> <conf> <write> <read>
rabbitmqctl set_permissions -p "/" admin ".*" ".*" ".*"
# 添加配置、写、读权限
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
docker update rabbitmq --restart=always
http://192.168.56.10:15672
guest
guest
<!--指定 jdk 编译版本--> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <configuration> <source>8</source> <target>8</target> </configuration> </plugin> </plugins> </build> <dependencies> <!--rabbitmq 依赖客户端--> <dependency> <groupId>com.rabbitmq</groupId> <artifactId>amqp-client</artifactId> <version>5.8.0</version> </dependency> <!--操作文件流的一个依赖--> <dependency> <groupId>commons-io</groupId> <artifactId>commons-io</artifactId> <version>2.6</version> </dependency> </dependencies>
public class Producer { private final static String QUEUE_NAME = "hello"; public static void main(String[] args) throws IOException, TimeoutException { // 创建一个连接工厂 ConnectionFactory connectionFactory = new ConnectionFactory(); // 设置配置 connectionFactory.setHost("192.168.56.10"); connectionFactory.setUsername("guest"); connectionFactory.setPassword("guest"); // 创建连接 Connection connection = connectionFactory.newConnection(); //获取信道 Channel channel = connection.createChannel(); /** * 生成一个队列 * 1.队列名称 * 2.队列里面的消息是否持久化 也就是是否用完就删除 * 3.该队列是否只供一个消费者进行消费 是否进行共享 true 可以多个消费者消费 * 4.是否自动删除 最后一个消费者端开连接以后 该队列是否自动删除 true 自动删除 * 5.其他参数 */ channel.queueDeclare(QUEUE_NAME, false, false, false, null); String message = "hello world _ zgc"; /** * 发送一个消息 * 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 IOException, TimeoutException { // 创建一个连接工厂 ConnectionFactory connectionFactory = new ConnectionFactory(); // 设置配置 connectionFactory.setHost("192.168.56.10"); connectionFactory.setUsername("guest"); connectionFactory.setPassword("guest"); // 创建连接 Connection connection = connectionFactory.newConnection(); //获取信道 Channel channel = connection.createChannel(); /** * 消费者消费消息 - 接受消息 * 1.消费哪个队列 * 2.消费成功之后是否要自动应答 true 代表自动应答 false 手动应答 * 3.消费者未成功消费的回调 * 4.消息被取消时的回调 */ channel.basicConsume(QUEUE_NAME, true, (var1, var2) -> { System.out.println(var1); System.out.println(new String(var2.getBody())); // amq.ctag-bQViL7Zs4P8NTpjcSHtWqQ //hello world _ zgc }, (var1) -> { System.out.println(var1); System.out.println("消息消费被中断"); }); } }
public class RabbitMqUtils { public static Channel getChannel() throws Exception { // 创建一个连接工厂 ConnectionFactory connectionFactory = new ConnectionFactory(); // 设置配置 connectionFactory.setHost("192.168.56.10"); connectionFactory.setUsername("guest"); connectionFactory.setPassword("guest"); // 创建连接 Connection connection = connectionFactory.newConnection(); //获取信道 Channel channel = connection.createChannel(); return channel; } }
public class Worker01 { private static final String QUEUE_NAME="hello"; public static void main(String[] args) throws Exception { Channel channel = RabbitMqUtils.getChannel(); /** * 消费者消费消息 - 接受消息 * 1.消费哪个队列 * 2.消费成功之后是否要自动应答 true 代表自动应答 false 手动应答 * 3.消费者未成功消费的回调 * 4.消息被取消时的回调 */ System.out.println("C3接受信息中..."); channel.basicConsume(QUEUE_NAME, true, (var1, var2) -> { System.out.println(var1); System.out.println(new String(var2.getBody())); }, (var1) -> { System.out.println(var1); System.out.println("消息消费被中断"); }); } }
public class Task1 { private static final String QUEUE_NAME="hello"; public static void main(String[] args) throws Exception { 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(); /** * 发送一个消息 * 1.发送到那个交换机 * 2.路由的 key 是哪个 * 3.其他的参数信息 * 4.发送消息的消息体 */ channel.basicPublish("", QUEUE_NAME, null, message.getBytes()); System.out.println("消息发送完成:" + message); } } }
true 代表批量应答 channel 上未应答的消息
false 同上面相比只会应答 tag=8 的消息 5,6,7 这三个消息依然不会被确认收到消息应答
public class Task2 { private static final String TASK_QUEUE_NAME ="ack_queue"; public static void main(String[] args) throws Exception { Channel channel = RabbitMqUtils.getChannel(); //声明队列 channel.queueDeclare(TASK_QUEUE_NAME, false, false, false, null); Scanner scanner = new Scanner(System.in); while (scanner.hasNext()){ String message = scanner.next(); /** * 发送一个消息 * 1.发送到那个交换机 * 2.路由的 key 是哪个 * 3.其他的参数信息 * 4.发送消息的消息体 */ channel.basicPublish("", TASK_QUEUE_NAME , null, message.getBytes()); System.out.println("消息发送完成:" + message); } } }
public class Worker02 { private static final String TASK_QUEUE_NAME = "ack_queue"; public static void main(String[] args) throws Exception { Channel channel = RabbitMqUtils.getChannel(); /** * 消费者消费消息 - 接受消息 * 1.消费哪个队列 * 2.消费成功之后是否要自动应答 true 代表自动应答 false 手动应答 * 3.消费者未成功消费的回调 * 4.消息被取消时的回调 */ System.out.println("C3接受信息中,120秒接受.."); //采用手动应答 boolean autoAck = false; channel.basicConsume(TASK_QUEUE_NAME , autoAck, (var1, var2) -> { try { Thread.sleep(12000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(new String(var2.getBody())); /** * 1.消息标记 tag * 2.是否批量应答未应答消息 */ channel.basicAck(var2.getEnvelope().getDeliveryTag(), false); }, (var1) -> { System.out.println(var1); System.out.println("消息消费被中断"); }); } }
//让队列持久化
boolean durable = true;
//声明队列
channel.queueDeclare(TASK_QUEUE_NAME, durable, false, false, null);
需要在消息生产者修改代码,MessageProperties.PERSISTENT_TEXT_PLAIN 添加这个属性。
将消息标记为持久化并不能完全保证不会丢失消息。尽管它告诉 RabbitMQ 将消息保存到磁盘,但是这里依然存在当消息刚准备存储在磁盘的时候 但是还没有存储完,消息还在缓存的一个间隔点。此时并没 有真正写入磁盘。持久性保证并不强,但是对于我们的简单任务队列而言,这已经绰绰有余了。
如果需要更强有力的持久化策略,参考后边课件发布确认章节。
在最开始的时候我们学习到 RabbitMQ 分发消息采用的轮训分发,但是在某种场景下这种策略并不是很好,比方说有两个消费者在处理任务,其中有个消费者 1 处理任务的速度非常快,而另外一个消费者 2 处理速度却很慢,这个时候我们还是采用轮训分发的化就会到这处理速度快的这个消费者很大一部分时间处于空闲状态,而处理慢的那个消费者一直在干活,这种分配方式在这种情况下其实就不太好,但是 RabbitMQ 并不知道这种情况它依然很公平的进行分发。
为了避免这种情况,在消费者中消费之前,我们可以设置参数 channel.basicQos(1);
//不公平分发
int prefetchCount = 1;
channel.basicQos(prefetchCount);
本身消息的发送就是异步发送的,所以在任何时候,channel 上肯定不止只有一个消息另外来自消费 者的手动确认本质上也是异步的。因此这里就存在一个未确认的消息缓冲区,因此希望开发人员能限制此缓冲区的大小,以避免缓冲区里面无限制的未确认消息问题。这个时候就可以通过使用 basic.qos 方法设 置“预取计数”值来完成的。
该值定义通道上允许的未确认消息的最大数量。一旦数量达到配置的数量, RabbitMQ 将停止在通道上传递更多消息,除非至少有一个未处理的消息被确认,例如,假设在通道上有未确认的消息 5、6、7,8,并且通道的预取计数设置为 4,此时RabbitMQ 将不会在该通道上再传递任何消息,除非至少有一个未应答的消息被 ack。比方说 tag=6 这个消息刚刚被确认 ACK,RabbitMQ 将会感知 这个情况到并再发送一条消息。消息应答和 QoS 预取值对用户吞吐量有重大影响。
通常,增加预取将提高 向消费者传递消息的速度。虽然自动应答传输消息速率是最佳的,但是,在这种情况下已传递但尚未处理的消息的数量也会增加,从而增加了消费者的 RAM 消耗(随机存取存储器)应该小心使用具有无限预处理的自动确认模式或手动确认模式,消费者消费了大量的消息如果没有确认的话,会导致消费者连接节点的 内存消耗变大,所以找到合适的预取值是一个反复试验的过程,不同的负载该值取值也不同 100 到 300 范 围内的值通常可提供最佳的吞吐量,并且不会给消费者带来太大的风险。
预取值为 1 是最保守的。当然这将使吞吐量变得很低,特别是消费者连接延迟很严重的情况下,特别是在消费者连接等待时间较长的环境 中。对于大多数应用来说,稍微高一点的值将是最佳的。
预取值极端为1就是不公平分发
生产者将信道设置成 confirm 模式,一旦信道进入 confirm 模式,所有在该信道上面发布的消息都将会被指派一个唯一的 ID(从 1 开始),一旦消息被投递到所有匹配的队列之后,broker 就会发送一个确认给生产者(包含消息的唯一 ID),这就使得生产者知道消息已经正确到达目的队列了,如果消息和队列是可持久化的,那么确认消息会在将消息写入磁盘之后发出,broker 回传给生产者的确认消息中 delivery-tag 域包含了确认消息的序列号,此外 broker 也可以设置basic.ack 的 multiple 域,表示到这个序列号之前的所有消息都已经得到了处理。
confirm 模式最大的好处在于他是异步的,一旦发布一条消息,生产者应用程序就可以在等信道返回确认的同时继续发送下一条消息,当消息最终得到确认之后,生产者应用便可以通过回调方法来处理该确认消息,如果RabbitMQ 因为自身内部错误导致消息丢失,就会发送一条 nack 消息, 生产者应用程序同样可以在回调方法中处理该 nack 消息。
完整的持久化(持久化只针对生产者这边)
//开启发布确认
channel.confirmSelect();
这是一种简单的确认方式,它是一种同步确认发布的方式,也就是发布一个消息之后只有它被确认发布,后续的消息才能继续发布,waitForConfirmsOrDie(long) 这个方法只有在消息被确认的时候才返回,如果在指定时间范围内这个消息没有被确认那么它将抛出异常。
这种确认方式有一个最大的缺点就是:发布速度特别的慢,因为如果没有确认发布的消息就会阻塞所有后续消息的发布,这种方式最多提供每秒不超过数百条发布消息的吞吐量。当然对于某些应用程序来说这可能已经足够了。
public class publishMessageIndividually { private static final int MESSAGE_COUNT = 1000; public static void main(String[] args) throws Exception { Channel channel = RabbitMqUtils.getChannel(); //队列声明 String queueName = UUID.randomUUID().toString(); channel.queueDeclare(queueName, true, false, false, null); //让队列持久化 boolean durable = true; //开启发布确认 channel.confirmSelect(); long begin = System.currentTimeMillis(); for (int i = 0; i < MESSAGE_COUNT; i++) { String message = String.valueOf(i); channel.basicPublish("", queueName, MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes()); //服务端返回 false 或超时时间内未返回,生产者可以消息重发 boolean flag = channel.waitForConfirms(); if (flag) { System.out.println(i + "消息发送成功"); } } long end = System.currentTimeMillis(); System.out.println("发布" + MESSAGE_COUNT + "个单独确认消息,耗时" + (end - begin) + "ms"); // 发布1000个单独确认消息,耗时1297ms } }
// 批量个确定发布 public class publishMessageIndividually2 { private static final int MESSAGE_COUNT = 1000; public static void main(String[] args) throws Exception { Channel channel = RabbitMqUtils.getChannel(); //队列声明 String queueName = UUID.randomUUID().toString(); channel.queueDeclare(queueName, true, false, false, null); //让队列持久化 boolean durable = true; //开启发布确认 channel.confirmSelect(); long begin = System.currentTimeMillis(); for (int i = 0; i < MESSAGE_COUNT; i++) { String message = String.valueOf(i); channel.basicPublish("", queueName, MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes()); //服务端返回 false 或超时时间内未返回,生产者可以消息重发 if(i % 100 == 0){ // 每过一百批量确认一次 boolean flag = channel.waitForConfirms(); if (flag) { System.out.println(i + "消息发送成功"); } } } long end = System.currentTimeMillis(); System.out.println("发布" + MESSAGE_COUNT + "个批量确认消息,耗时" + (end - begin) + "ms"); // 发布1000个批量确认消息,耗时67ms } }
// 异步确定发布 public class publishMessageIndividually3 { private static final int MESSAGE_COUNT = 1000; public static void main(String[] args) throws Exception { Channel channel = RabbitMqUtils.getChannel(); //队列声明 String queueName = UUID.randomUUID().toString(); channel.queueDeclare(queueName, true, false, false, null); //让队列持久化 boolean durable = true; //开启发布确认 channel.confirmSelect(); /** * 线程安全有序的一个哈希表,适用于高并发的情况 * 1.轻松的将序号与消息进行关联 * 2.轻松批量删除条目 只要给到序列号 * 3.支持并发访问 */ ConcurrentSkipListMap<Long,String> concurrentSkipListMap = new ConcurrentSkipListMap(); /** * 确认收到消息的一个回调 * 1.long l 消息序列号 * 2. boolean b * true 可以确认小于等于当前序列号的消息 * false 确认当前序列号消息 */ ConfirmCallback ackCallback = (long l, boolean b) ->{ if (b){ // 小于等于当前序列号的消息 System.out.println(l); // 返回的是小于等于当前序列号的未确认消息 是一个 map ConcurrentNavigableMap<Long, String> longStringConcurrentNavigableMap = concurrentSkipListMap.headMap(l, true); // 清除该部分未确认消息 longStringConcurrentNavigableMap.clear(); // 比如返回50,但凡小于或者等于50的key都是发布成功的 = 但凡小于或者等于50的key在队列中都要消失, }else { // 确认当前序列号消息 // 只清除当前序列号的消息 concurrentSkipListMap.remove(l); } System.out.println(concurrentSkipListMap.size()); }; /** * 添加一个异步确认的监听器 * 1.确认收到消息的回调 * 2.未收到消息的回调 */ channel.addConfirmListener(ackCallback,null); long begin = System.currentTimeMillis(); for (int i = 0; i < MESSAGE_COUNT; i++) { String message = String.valueOf(i); channel.basicPublish("", queueName, MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes()); // 存放队列 concurrentSkipListMap.put(channel.getNextPublishSeqNo(),message); } long end = System.currentTimeMillis(); System.out.println("发布" + MESSAGE_COUNT + "个异步确认消息,耗时" + (end - begin) + "ms"); // 发布1000个异步确认消息,耗时32ms } }
单独发布消息
批量发布消息
异步处理
RabbitMQ 消息传递模型的核心思想是: 生产者生产的消息从不会直接发送到队列。实际上,通常生产者甚至都不知道这些消息传递传递到了哪些队列中。
相反,生产者只能将消息发送到交换机(exchange),交换机工作的内容非常简单,一方面它接收来自生产者的消息,另一方面将它们推入队列。交换机必须确切知道如何处理收到的消息。是应该把这些消息放到特定队列还是说把他们到许多队列中还是说应该丢弃它们。这就的由交换机的类型来决定。
每当我们连接到 Rabbit 时,我们都需要一个全新的空队列,为此我们可以创建一个具有随机名称的队列,或者能让服务器为我们选择一个随机队列名称那就更好了。其次一旦我们断开了消费者的连接,队列将被自动删除。
创建临时队列的方式如下:
String queueName = channel.queueDeclare().getQueue();
// 生产者 public class EmitLog { private static final String EXCHANGE_NAME = "logs"; public static void main(String[] args) throws Exception { Channel channel = RabbitMqUtils.getChannel(); /** * 声明一个 exchange * 1.exchange 的名称 * 2.exchange 的类型 */ channel.exchangeDeclare(EXCHANGE_NAME,"fanout"); Scanner scanner = new Scanner(System.in); System.out.println("请输入信息:"); while (scanner.hasNext()){ String message = scanner.next(); channel.basicPublish(EXCHANGE_NAME,"",null,message.getBytes()); } } }
// 消费者 public class ReceiveLogs01 { private static final String EXCHANGE_NAME = "logs"; public static void main(String[] args) throws Exception { Channel channel = RabbitMqUtils.getChannel(); /** * 声明一个 exchange * 1.exchange 的名称 * 2.exchange 的类型 */ channel.exchangeDeclare(EXCHANGE_NAME, "fanout"); /** * 生成一个临时的队列 队列的名称是随机的 * 当消费者断开和该队列的连接时 队列自动删除 */ String queue = channel.queueDeclare().getQueue(); //把该临时队列绑定我们的 exchange 其中 routingkey(也称之为 binding key)为空字符串 channel.queueBind(queue, EXCHANGE_NAME, ""); System.out.println("消费者1在接受信息:" ); channel.basicConsume(queue, true, (consumerTag, delivery) -> { String s = new String(delivery.getBody(), "UTF-8"); System.out.println("控制台打印接收到的消息" + s); }, (var1) -> { }); } }
交换机的声明,可以是生产者也可以是消费者,也可以一起声明
// 生产者 public class EmitLog { private static final String EXCHANGE_NAME = "direct_logs"; public static void main(String[] args) throws Exception { Channel channel = RabbitMqUtils.getChannel(); /** * 声明一个 exchange * 1.exchange 的名称 * 2.exchange 的类型 */ channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT); String key = "warning"; Scanner scanner = new Scanner(System.in); System.out.println("请输入信息:"); while (scanner.hasNext()){ String message = scanner.next(); System.out.println("发送key"+key+"的信息"); channel.basicPublish(EXCHANGE_NAME,key,null,message.getBytes()); } } }
// 消费者 public class ReceiveLogs01 { private static final String EXCHANGE_NAME = "direct_logs"; public static void main(String[] args) throws Exception { Channel channel = RabbitMqUtils.getChannel(); /** * 声明一个 exchange * 1.exchange 的名称 * 2.exchange 的类型 */ channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT); /** * 生成一个临时的队列 队列的名称是随机的 * 当消费者断开和该队列的连接时 队列自动删除 */ String queue = channel.queueDeclare().getQueue(); //把该临时队列绑定我们的 exchange 其中 routingkey(也称之为 binding key)为空字符串 channel.queueBind(queue, EXCHANGE_NAME, "error"); channel.queueBind(queue, EXCHANGE_NAME, "warning"); System.out.println("消费者1,key为error和warning在接受信息:" ); channel.basicConsume(queue, true, (consumerTag, delivery) -> { String s = new String(delivery.getBody(), "UTF-8"); System.out.println("控制台打印接收到的消息" + s); }, (var1) -> { }); } }
// 消费者 public class ReceiveLogs02 { private static final String EXCHANGE_NAME = "direct_logs"; public static void main(String[] args) throws Exception { Channel channel = RabbitMqUtils.getChannel(); /** * 声明一个 exchange * 1.exchange 的名称 * 2.exchange 的类型 */ channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT); /** * 生成一个临时的队列 队列的名称是随机的 * 当消费者断开和该队列的连接时 队列自动删除 */ String queue = channel.queueDeclare().getQueue(); //把该临时队列绑定我们的 exchange 其中 routingkey(也称之为 binding key)为空字符串 channel.queueBind(queue, EXCHANGE_NAME, "info"); channel.queueBind(queue, EXCHANGE_NAME, "warning"); System.out.println("消费者2,key为info和warning在接受信息:" ); channel.basicConsume(queue, true, (consumerTag, delivery) -> { String s = new String(delivery.getBody(), "UTF-8"); System.out.println("控制台打印接收到的消息" + s); }, (var1) -> { }); } }
发送到类型是 topic 交换机的消息的 routing_key 不能随意写,必须满足一定的要求,它必须是一个单词列表,以点号分隔开。这些单词可以是任意单词
比如说:“stock.usd.nyse”, “nyse.vmw”, “quick.orange.rabbit”.这种类型的。
当然这个单词列表最多不能超过 255 个字节。
在这个规则列表中,其中有两个替换符是大家需要注意的:
Q1–>绑定的是
Q2–>绑定的是
// 生产者 public class EmitLog { private static final String EXCHANGE_NAME = "topic_logs"; public static void main(String[] args) throws Exception { Channel channel = RabbitMqUtils.getChannel(); /** * 声明一个 exchange * 1.exchange 的名称 * 2.exchange 的类型 */ channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.TOPIC); /** * Q1-->绑定的是 * 中间带 orange 带 3 个单词的字符串(*.orange.*) * Q2-->绑定的是 * 最后一个单词是 rabbit 的 3 个单词(*.*.rabbit) * 第一个单词是 lazy 的多个单词(lazy.#) * */ Map<String, String> bindingKeyMap = new HashMap<>(); bindingKeyMap.put("quick.orange.rabbit", "被队列 Q1Q2 接收到"); bindingKeyMap.put("lazy.orange.elephant", "被队列 Q1Q2 接收到"); bindingKeyMap.put("quick.orange.fox", "被队列 Q1 接收到"); bindingKeyMap.put("lazy.brown.fox", "被队列 Q2 接收到"); bindingKeyMap.put("lazy.pink.rabbit", "虽然满足两个绑定但只被队列 Q2 接收一次"); bindingKeyMap.put("quick.brown.fox", "不匹配任何绑定不会被任何队列接收到会被丢弃"); bindingKeyMap.put("quick.orange.male.rabbit", "是四个单词不匹配任何绑定会被丢弃"); bindingKeyMap.put("lazy.orange.male.rabbit", "是四个单词但匹配 Q2"); for (Map.Entry<String, String> bindingKeyEntry : bindingKeyMap.entrySet()) { channel.basicPublish(EXCHANGE_NAME, bindingKeyEntry.getKey(), null, bindingKeyEntry.getValue().getBytes("UTF-8")); System.out.println("生产者发出消息:" + bindingKeyEntry.getValue()); } } }
// 消费者 public class ReceiveLogs01 { private static final String EXCHANGE_NAME = "topic_logs"; public static void main(String[] args) throws Exception { Channel channel = RabbitMqUtils.getChannel(); /** * 声明一个 exchange * 1.exchange 的名称 * 2.exchange 的类型 */ channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.TOPIC); /** * 生成一个队列 */ String queueName = "Q1"; channel.queueDeclare(queueName,false, false, false, null); channel.queueBind(queueName,EXCHANGE_NAME,"*.orange.*"); System.out.println("Q1:*.orange.*:接受消息..."); channel.basicConsume(queueName, true, (consumerTag, delivery) -> { String s = new String(delivery.getBody(), "UTF-8"); System.out.println("控制台打印接收到的消息" + s); }, (var1) -> { }); } }
// 消费者 public class ReceiveLogs02 { private static final String EXCHANGE_NAME = "topic_logs"; public static void main(String[] args) throws Exception { Channel channel = RabbitMqUtils.getChannel(); /** * 声明一个 exchange * 1.exchange 的名称 * 2.exchange 的类型 */ channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.TOPIC); /** * 生成一个队列 */ String queueName = "Q2"; channel.queueDeclare(queueName,false, false, false, null); channel.queueBind(queueName,EXCHANGE_NAME,"*.*.rabbit"); channel.queueBind(queueName, EXCHANGE_NAME, "lazy.#"); System.out.println("Q2:*.*.rabbit和lazy.#:接受消息..."); channel.basicConsume(queueName, true, (consumerTag, delivery) -> { String s = new String(delivery.getBody(), "UTF-8"); System.out.println("控制台打印接收到的消息" + s); }, (var1) -> { }); } }
先从概念解释上搞清楚这个定义,死信,顾名思义就是无法被消费的消息,字面意思可以这样理 解,一般来说,producer 将消息投递到 broker 或者直接到queue 里了,consumer 从 queue 取出消息 进行消费,但某些时候由于特定的原因导致 queue 中的某些消息无法被消费,这样的消息如果没有后续的处理,就变成了死信,有死信自然就有了死信队列。
应用场景:为了保证订单业务的消息数据不丢失,需要使用到 RabbitMQ 的死信队列机制,当消息消费发生异常时,将消息投入死信队列中。还有比如说:用户在商城下单成功并点击去支付后在指定时间未支付时自动失效
消息 TTL 过期
队列达到最大长度
消息被拒绝并且不重新放入队列
// 生产者 public class EmitLog { //普通交换机名称 private static final String NORMAL_EXCHANGE = "normal_exchange"; //死信交换机名称 private static final String DEAD_EXCHANGE = "dead_exchange"; //普通队列 private static final String NORMAL_QUEUE = "normal_queue"; //死信队列 private static final String DEAD_QUEUE = "dead_queue"; //普通key private static final String NORMAL_KEY = "zhangsan"; //死信队key private static final String DEAD_KEY = "lisi"; public static void main(String[] args) throws Exception { Channel channel = RabbitMqUtils.getChannel(); // 交换机声明 channel.exchangeDeclare(NORMAL_EXCHANGE, BuiltinExchangeType.DIRECT); //设置消息的 TTL 时间 10s AMQP.BasicProperties properties = new AMQP.BasicProperties().builder().expiration("10000").build(); //该信息是用作演示队列个数限制 for (int i = 1; i < 11; i++) { String message = "info" + i; // Thread.sleep(1000); channel.basicPublish(NORMAL_EXCHANGE, NORMAL_KEY, properties, message.getBytes()); System.out.println("生产者发送消息:" + message); } } }
// 消费者-正常消费者 public class ReceiveLogs01 { //普通交换机名称 private static final String NORMAL_EXCHANGE = "normal_exchange"; //死信交换机名称 private static final String DEAD_EXCHANGE = "dead_exchange"; //普通队列 private static final String NORMAL_QUEUE = "normal_queue"; //死信队列 private static final String DEAD_QUEUE = "dead_queue"; //普通key private static final String NORMAL_KEY = "zhangsan"; //死信队key private static final String DEAD_KEY = "lisi"; public static void main(String[] args) throws Exception { Channel channel = RabbitMqUtils.getChannel(); // 声明交换机 channel.exchangeDeclare(NORMAL_EXCHANGE, BuiltinExchangeType.DIRECT); channel.exchangeDeclare(DEAD_EXCHANGE, BuiltinExchangeType.DIRECT); // 正常队列绑定死信队列信息 Map<String, Object> params = new HashMap<>(); // 正常队列设置死信交换机 参数 key 是固定值 params.put("x-dead-letter-exchange", DEAD_EXCHANGE); // 正常队列设置死信 routing-key 参数 key 是固定值 params.put("x-dead-letter-routing-key", DEAD_KEY); // 设置正常队列的长度限制,例如发10个,4个则为死信 params.put("x-max-length", 6); // 过期时间,但一般在生产者那边设置 // 队列设置,只能是固定统一, // 生产者设置,可以不一致 // 声明队列 channel.queueDeclare(NORMAL_QUEUE, false, false, false, params); channel.queueBind(NORMAL_QUEUE, NORMAL_EXCHANGE, NORMAL_KEY); channel.queueDeclare(DEAD_QUEUE, false, false, false, null); channel.queueBind(DEAD_QUEUE, DEAD_EXCHANGE, DEAD_KEY); System.out.println("C1普通队列,等待接收消息........... "); channel.basicConsume(NORMAL_QUEUE, false, (consumerTag, delivery) -> { String s = new String(delivery.getBody(), "UTF-8"); if (s.equals("info5")) { System.out.println("接收到消息" + s + "并拒绝签收该消息"); //requeue 设置为 false 代表拒绝重新入队 该队列如果配置了死信交换机将发送到死信队列中 channel.basicReject(delivery.getEnvelope().getDeliveryTag(), false); } else { System.out.println("正常接收到消息" + s); channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false); } }, (var1) -> { }); } }
// 消费者-死信消费者 public class ReceiveLogs02 { //普通交换机名称 private static final String NORMAL_EXCHANGE = "normal_exchange"; //死信交换机名称 private static final String DEAD_EXCHANGE = "dead_exchange"; //普通队列 private static final String NORMAL_QUEUE = "normal_queue"; //死信队列 private static final String DEAD_QUEUE = "dead_queue"; //普通key private static final String NORMAL_KEY = "zhangsan"; //死信队key private static final String DEAD_KEY = "lisi"; public static void main(String[] args) throws Exception { Channel channel = RabbitMqUtils.getChannel(); // 声明交换机 channel.exchangeDeclare(DEAD_EXCHANGE, BuiltinExchangeType.DIRECT); // 声明队列 channel.queueDeclare(DEAD_QUEUE, false, false, false, null); channel.queueBind(DEAD_QUEUE, DEAD_EXCHANGE, DEAD_KEY); System.out.println("C2死信队列,等待接收消息........... "); channel.basicConsume(DEAD_QUEUE, true, (consumerTag, delivery) -> { String s = new String(delivery.getBody(), "UTF-8"); System.out.println("接收到消息" + s); }, (var1) -> { }); } }
结果展示-拒收并不重新放入队列
结果展示-队列达到最大长度
结果展示-消息 TTL 过期
TTL 是什么呢?TTL 是 RabbitMQ 中一个消息或者队列的属性,表明一条消息或者该队列中的所有消息的最大存活时间,单位是毫秒。
换句话说,如果一条消息设置了 TTL 属性或者进入了设置TTL 属性的队列,那么这条消息如果在 TTL 设置的时间内没有被消费,则会成为"死信"。如果同时配置了队列的TTL 和消息的 TTL,那么较小的那个值将会被使用,有两种方式设置 TTL。
如果设置了队列的 TTL 属性,那么一旦消息过期,就会被队列丢弃(如果配置了死信队列被丢到死信队列中),而第二种方式,消息即使过期,也不一定会被马上丢弃,因为消息是否过期是在即将投递到消费者之前判定的,如果当前队列有严重的消息积压情况,则已过期的消息也许还能存活较长时间;
另外,还需要注意的一点是,如果不设置 TTL,表示消息永远不会过期,如果将 TTL 设置为 0,则表示除非此时可以直接投递该消息到消费者,否则该消息将会被丢弃。
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <!--RabbitMQ 依赖--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-amqp</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.47</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> <!--swagger--> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger2</artifactId> <version>3.0.0</version> </dependency> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger-ui</artifactId> <version>3.0.0</version> </dependency> <!--RabbitMQ 测试依赖--> <dependency> <groupId>org.springframework.amqp</groupId> <artifactId>spring-rabbit-test</artifactId> <scope>test</scope> </dependency> </dependencies>
spring.rabbitmq.host=192.168.56.10
spring.rabbitmq.port=5672
spring.rabbitmq.username=admin
spring.rabbitmq.password=123456
@Configuration @EnableSwagger2 public class SwaggerConfig { @Bean public Docket webApiConfig() { return new Docket(DocumentationType.SWAGGER_2) .groupName("webApi") .apiInfo(webApiInfo()) .select() .build(); } private ApiInfo webApiInfo() { return new ApiInfoBuilder() .title("rabbitmq 接口文档") .description("本文档描述了 rabbitmq 微服务接口定义") .version("1.0") .contact(new Contact("zhiyuan", "http://oddfar.com", "test@qq.com")) .build(); } }
@Configuration public class TtlQueueConfig { // 两正常队列的交换机 public static final String X_EXCHANGE = "X"; // 正常队列的队列 public static final String QUEUE_A = "QA"; // 正常队列的队列 public static final String QUEUE_B = "QB"; // 延时交换机 public static final String Y_DEAD_LETTER_EXCHANGE = "Y"; // 延时队列 public static final String DEAD_LETTER_QUEUE = "QD"; // 正常队列KEY public static final String QUEUE_A_KEY = "XA"; // 正常队列KEY public static final String QUEUE_B_KEY = "XB"; // 延时队列KEY public static final String DEAD_QUEUE_KEY = "YD"; // 声明交换机 @Bean("xExchange") public DirectExchange xExchange() { return new DirectExchange(X_EXCHANGE); } // 声明交换机 @Bean("yExchange") public DirectExchange yExchange() { return new DirectExchange(Y_DEAD_LETTER_EXCHANGE); } // 声明队列 A // ttl 为 10s,同时绑定延时交换机 @Bean("queueA") public Queue queueA() { Map<String, Object> args = new HashMap<>(); //声明当前队列绑定的死信交换机 args.put("x-dead-letter-exchange", Y_DEAD_LETTER_EXCHANGE); //声明当前队列的死信路由 key args.put("x-dead-letter-routing-key", DEAD_QUEUE_KEY); //声明队列的 TTL args.put("x-message-ttl", 10000); return QueueBuilder.durable(QUEUE_A).withArguments(args).build(); } // 声明队列 B // ttl 为 40s,同时绑定延时交换机 @Bean("queueB") public Queue queueB() { Map<String, Object> args = new HashMap<>(); //声明当前队列绑定的死信交换机 args.put("x-dead-letter-exchange", Y_DEAD_LETTER_EXCHANGE); //声明当前队列的死信路由 key args.put("x-dead-letter-routing-key", DEAD_QUEUE_KEY); //声明队列的 TTL args.put("x-message-ttl", 40000); return QueueBuilder.durable(QUEUE_B).withArguments(args).build(); } // 声明死信队列 QD @Bean("queueD") public Queue queueD() { return new Queue(DEAD_LETTER_QUEUE); } //声明队列 A 绑定 X 交换机 @Bean public Binding queueABindingX(@Qualifier("queueA") Queue queueA, @Qualifier("xExchange") DirectExchange xExchange) { return BindingBuilder.bind(queueA).to(xExchange).with(QUEUE_A_KEY); } //声明队列 B 绑定 X 交换机 @Bean public Binding queueBBindingX(@Qualifier("queueB") Queue queueB, @Qualifier("xExchange") DirectExchange xExchange) { return BindingBuilder.bind(queueB).to(xExchange).with(QUEUE_B_KEY); } // 声明死信队列 QD 绑定关系 @Bean public Binding deadLetterBindingQAD(@Qualifier("queueD") Queue queueD, @Qualifier("yExchange") DirectExchange yExchange) { return BindingBuilder.bind(queueD).to(yExchange).with(DEAD_QUEUE_KEY); } }
@Slf4j @RequestMapping("ttl") @RestController public class SendMsgController { @Autowired private RabbitTemplate rabbitTemplate; private static final Logger logger = LoggerFactory.getLogger(SendMsgController.class); @GetMapping("sendMsg/{message}") public void sendMsg(@PathVariable String message) { logger.info("当前时间:{},发送一条信息给两个 TTL 队列:{}", new Date(), message); rabbitTemplate.convertAndSend("X", "XA", "消息来自 ttl 为 10S 的队列: " + message); rabbitTemplate.convertAndSend("X", "XB", "消息来自 ttl 为 40S 的队列: " + message); } }
@Slf4j
@Component
public class DeadLetterQueueConsumer {
private static final Logger logger = LoggerFactory.getLogger(SendMsgController.class);
@RabbitListener(queues = "QD")
public void receiveD(Message message, Channel channel) throws IOException {
String msg = new String(message.getBody());
logger.info("当前时间:{},收到死信队列信息{}", new Date().toString(), msg);
}
}
// 延时队列-优化 @Component public class MsgTtlQueueConfig { public static final String Y_DEAD_LETTER_EXCHANGE = "Y"; public static final String QUEUE_C = "QC"; //声明队列 C 死信交换机 @Bean("queueC") public Queue queueB() { Map<String, Object> args = new HashMap<>(3); //声明当前队列绑定的死信交换机 args.put("x-dead-letter-exchange", Y_DEAD_LETTER_EXCHANGE); //声明当前队列的死信路由 key args.put("x-dead-letter-routing-key", "YD"); //没有声明 TTL 属性 return QueueBuilder.durable(QUEUE_C).withArguments(args).build(); } //声明队列 B 绑定 X 交换机 @Bean public Binding queuecBindingX(@Qualifier("queueC") Queue queueC, @Qualifier("xExchange") DirectExchange xExchange) { return BindingBuilder.bind(queueC).to(xExchange).with("XC"); } }
@GetMapping("sendExpirationMsg/{message}/{ttlTime}")
public void sendMsg(@PathVariable String message,@PathVariable String ttlTime) {
rabbitTemplate.convertAndSend("X", "XC", message, correlationData ->{
// 设置过期时间
correlationData.getMessageProperties().setExpiration(ttlTime);
return correlationData;
});
logger.info("当前时间:{},发送一条时长{}毫秒 TTL 信息给队列 C:{}", new Date(),ttlTime, message);
}
过期时间更快先发
过期时间更慢先发
/usr/lib/rabbitmq/lib/rabbitmq_server-3.8.8/plugins
rabbitmq-plugins enable rabbitmq_delayed_message_exchange
// 查看rabbitmq版本
docker image inspect rabbitmq:management|grep -i version
// 下载
// 复制进容器
docker cp rabbitmq_delayed_message_exchange-3.9.0.ez rabbitmq:plugins
// 进入容器
docker exec -it rabbitmq /bin/bash
// 插件目录
cd plugins
// 安装插件
rabbitmq-plugins enable rabbitmq_delayed_message_exchange
// 重启
docker restart rabbitmq
图解
配置类
// 延时队列-优化-插件 @Component public class DelayedQueueConfig { public static final String DELAYED_QUEUE_NAME = "delayed.queue"; public static final String DELAYED_EXCHANGE_NAME = "delayed.exchange"; public static final String DELAYED_ROUTING_KEY = "delayed.routingkey"; @Bean public Queue delayedQueue() { return new Queue(DELAYED_QUEUE_NAME); } //自定义交换机 我们在这里定义的是一个延迟交换机 @Bean public CustomExchange delayedExchange() { Map<String, Object> args = new HashMap<>(); //自定义交换机的类型 args.put("x-delayed-type", "direct"); return new CustomExchange(DELAYED_EXCHANGE_NAME, "x-delayed-message", true, false, args); } @Bean public Binding bindingDelayedQueue(@Qualifier("delayedQueue") Queue queue, @Qualifier("delayedExchange") CustomExchange delayedExchange) { return BindingBuilder.bind(queue).to(delayedExchange).with(DELAYED_ROUTING_KEY).noargs(); } }
@GetMapping("sendDelayMsg/{message}/{delayTime}")
public void sendMsg(@PathVariable String message, @PathVariable Integer delayTime) {
rabbitTemplate.convertAndSend("delayed.exchange", "delayed.routingkey", message,
correlationData -> {
// 设置过期时间
correlationData.getMessageProperties().setDelay(delayTime);
return correlationData;
});
logger.info(" 当 前 时 间 : {}, 发 送 一 条 延 迟 {} 毫秒的信息给队列 delayed.queue:{}", new Date(), delayTime, message);
}
@RabbitListener(queues = "delayed.queue")
public void receiveDelayedQueue(Message message){
String msg = new String(message.getBody());
logger.info("当前时间:{},收到延时队列的消息:{}", new Date().toString(), msg);
}
延时队列在需要延时处理的场景下非常有用,使用 RabbitMQ 来实现延时队列可以很好的利用 RabbitMQ 的特性,如:消息可靠发送、消息可靠投递、死信队列来保障消息至少被消费一次以及未被正确处理的消息不会被丢弃。另外,通过 RabbitMQ 集群的特性,可以很好的解决单点故障问题,不会因为 单个节点挂掉导致延时队列不可用或者消息丢失。
当然,延时队列还有很多其它选择,比如利用 Java 的 DelayQueue,利用 Redis 的 zset,利用 Quartz 或者利用 kafka 的时间轮,这些方式各有特点,看需要适用的场景
# 发布确认模式:发布消息成功到交换器后会触发回调方法
spring.rabbitmq.publisher-confirm-type=correlated
NONE 值是禁用发布确认模式,是默认值
CORRELATED 值是发布消息成功到交换器后会触发回调方法
SIMPLE 值经测试有两种效果,其一效果和 CORRELATED 值一样会触发回调方法,其二在发布消息成功后使用 rabbitTemplate 调用 waitForConfirms 或 waitForConfirmsOrDie 方法等待 broker 节点返回发送结果,根据返回结果来判定下一步的逻辑,要注意的点是 waitForConfirmsOrDie 方法如果返回 false 则会关闭 channel,则接下来无法发送消息到 broker;
@Configuration public class ConfirmConfig { public static final String CONFIRM_EXCHANGE_NAME = "confirm.exchange"; public static final String CONFIRM_QUEUE_NAME = "confirm.queue"; //声明业务 Exchange @Bean("confirmExchange") public DirectExchange confirmExchange() { return new DirectExchange(CONFIRM_EXCHANGE_NAME); } // 声明确认队列 @Bean("confirmQueue") public Queue confirmQueue() { return QueueBuilder.durable(CONFIRM_QUEUE_NAME).build(); } // 声明确认队列绑定关系 @Bean public Binding queueBinding(@Qualifier("confirmQueue") Queue queue, @Qualifier("confirmExchange") DirectExchange exchange) { return BindingBuilder.bind(queue).to(exchange).with("key1"); } }
@RestController @RequestMapping("/confirm") @Slf4j public class Producer { private static final Logger logger = LoggerFactory.getLogger(SendMsgController.class); public static final String CONFIRM_EXCHANGE_NAME = "confirm.exchange"; @Autowired private RabbitTemplate rabbitTemplate; @Autowired private MyCallBack myCallBack; //依赖注入 rabbitTemplate 之后再设置它的回调对象 @PostConstruct public void init() { rabbitTemplate.setConfirmCallback(myCallBack); } @GetMapping("sendMessage/{message}") public void sendMessage(@PathVariable String message) { //指定消息 id 为 1 // 正常信息 CorrelationData correlationData1 = new CorrelationData("1"); String routingKey = "key1"; rabbitTemplate.convertAndSend(CONFIRM_EXCHANGE_NAME, routingKey, message + routingKey, correlationData1); // RoutingKey 和BindingKey 不一致 CorrelationData correlationData2 = new CorrelationData("2"); routingKey = "key2"; rabbitTemplate.convertAndSend(CONFIRM_EXCHANGE_NAME, routingKey, message + routingKey, correlationData2); // 交换机不存在 CorrelationData correlationData3 = new CorrelationData("3"); routingKey = "key3"; rabbitTemplate.convertAndSend(CONFIRM_EXCHANGE_NAME+"jia", routingKey, message + routingKey, correlationData3); logger.info("发送消息内容:{}", message); } }
@Component
@Slf4j
public class ConfirmConsumer {
private static final Logger logger = LoggerFactory.getLogger(SendMsgController.class);
public static final String CONFIRM_QUEUE_NAME = "confirm.queue";
@RabbitListener(queues = CONFIRM_QUEUE_NAME)
public void receiveMsg(Message message) {
String msg = new String(message.getBody());
logger.info("接受到队列 confirm.queue 消息:{}", msg);
}
}
@Component @Slf4j public class MyCallBack implements RabbitTemplate.ConfirmCallback { private static final Logger logger = LoggerFactory.getLogger(SendMsgController.class); /** * 交换机不管是否收到消息的一个回调方法 * CorrelationData * 消息相关数据 * ack * 交换机是否收到消息 */ @Override public void confirm(CorrelationData correlationData, boolean ack, String cause) { String id = correlationData != null ? correlationData.getId() : ""; if (ack) { logger.info("交换机已经收到 id 为:{}的消息", id); } else { logger.info("交换机还未收到 id 为:{}消息,由于原因:{}", id, cause); } } }
/**
* true:交换机无法将消息进行路由时,会将该消息返回给生产者
* false:如果发现消息无法进行路由,则直接丢弃
*/
rabbitTemplate.setMandatory(true);
//设置回退消息交给谁处理
rabbitTemplate.setReturnsCallback(myCallBack);
#消息退回
spring.rabbitmq.publisher-returns=true
@RestController @RequestMapping("/confirm") @Slf4j public class Producer { private static final Logger logger = LoggerFactory.getLogger(SendMsgController.class); public static final String CONFIRM_EXCHANGE_NAME = "confirm.exchange"; @Autowired private RabbitTemplate rabbitTemplate; @Autowired private MyCallBack myCallBack; //依赖注入 rabbitTemplate 之后再设置它的回调对象 @PostConstruct public void init() { rabbitTemplate.setConfirmCallback(myCallBack); /** * true:交换机无法将消息进行路由时,会将该消息返回给生产者 * false:如果发现消息无法进行路由,则直接丢弃 */ rabbitTemplate.setMandatory(true); //设置回退消息交给谁处理 rabbitTemplate.setReturnsCallback(myCallBack); } @GetMapping("sendMessage/{message}") public void sendMessage(@PathVariable String message) { //指定消息 id 为 1 // 正常信息 CorrelationData correlationData1 = new CorrelationData("1"); String routingKey = "key1"; rabbitTemplate.convertAndSend(CONFIRM_EXCHANGE_NAME, routingKey, message + routingKey, correlationData1); // RoutingKey 和BindingKey 不一致 CorrelationData correlationData2 = new CorrelationData("2"); routingKey = "key2"; rabbitTemplate.convertAndSend(CONFIRM_EXCHANGE_NAME, routingKey, message + routingKey, correlationData2); // 交换机不存在 CorrelationData correlationData3 = new CorrelationData("3"); routingKey = "key3"; rabbitTemplate.convertAndSend(CONFIRM_EXCHANGE_NAME+"jia", routingKey, message + routingKey, correlationData3); logger.info("发送消息内容:{}", message); } }
@Component @Slf4j public class MyCallBack implements RabbitTemplate.ConfirmCallback, RabbitTemplate.ReturnsCallback { private static final Logger logger = LoggerFactory.getLogger(SendMsgController.class); /** * 交换机不管是否收到消息的一个回调方法 * CorrelationData * 消息相关数据 * ack * 交换机是否收到消息 */ @Override public void confirm(CorrelationData correlationData, boolean ack, String cause) { String id = correlationData != null ? correlationData.getId() : ""; if (ack) { logger.info("交换机已经收到 id 为:{}的消息", id); } else { logger.info("交换机还未收到 id 为:{}消息,由于原因:{}", id, cause); } } /* //当消息无法路由的时候的回调方法(旧,抛弃) @Override public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) { logger.error(" 消 息 {}, 被交换机 {} 退回,退回原因 :{}, 路 由 key:{}", new String(message.getBody()), exchange, replyText, routingKey); } */ //当消息无法路由的时候的回调方法 @Override public void returnedMessage(ReturnedMessage returned) { logger.error("消息:{},被交换机 {} 退回,原因:{},路由key:{},code:{}", new String(returned.getMessage().getBody()), returned.getExchange(), returned.getReplyText(), returned.getRoutingKey(), returned.getReplyCode()); } }
有了 mandatory 参数和回退消息,我们获得了对无法投递消息的感知能力,在生产者的消息无法被投递时发现并处理。但有时候,我们并不知道该如何处理这些无法路由的消息,最多打个日志,然后触发报警,再来手动处理。而通过日志来处理这些无法路由的消息是很不优雅的做法,特别是当生产者所在的服务有多台机器的时候,手动复制日志会更加麻烦而且容易出错。而且设置 mandatory 参数会增加生产者的复杂性,需要添加处理这些被退回的消息的逻辑。如果既不想丢失消息,又不想增加生产者的复杂性,该怎么做呢?
前面在设置死信队列的文章中,我们提到,可以为队列设置死信交换机来存储那些处理失败的消息,可是这些不可路由消息根本没有机会进入到队列,因此无法使用死信队列来保存消息。 在 RabbitMQ 中,有一种备份交换机的机制存在,可以很好的应对这个问题。
什么是备份交换机呢?备份交换机可以理解为 RabbitMQ 中交换机的“备胎”,当我们为某一个交换机声明一个对应的备份交换机时,就是为它创建一个备胎,当交换机接收到一条不可路由消息时,将会把这条消息转发到备份交换机中,由备份交换机来进行转发和处理,通常备份交换机的类型为 Fanout ,这样就能把所有消息都投递到与其绑定的队列中,然后我们在备份交换机下绑定一个队列,这样所有那些原交换机无法被路由的消息,就会都进 入这个队列了。当然,我们还可以建立一个报警队列,用独立的消费者来进行监测和报警。
@Configuration public class ConfirmConfig { public static final String CONFIRM_EXCHANGE_NAME = "confirm.exchange"; public static final String CONFIRM_QUEUE_NAME = "confirm.queue"; //关于备份的 public static final String BACKUP_EXCHANGE_NAME = "backup.exchange"; public static final String BACKUP_QUEUE_NAME = "backup.queue"; public static final String WARNING_QUEUE_NAME = "warning.queue"; /* //声明业务 Exchange @Bean("confirmExchange") public DirectExchange confirmExchange() { return new DirectExchange(CONFIRM_EXCHANGE_NAME); }*/ // 声明确认队列 @Bean("confirmQueue") public Queue confirmQueue() { return QueueBuilder.durable(CONFIRM_QUEUE_NAME).build(); } // 声明确认队列绑定关系 @Bean public Binding queueBinding(@Qualifier("confirmQueue") Queue queue, @Qualifier("confirmExchange") DirectExchange exchange) { return BindingBuilder.bind(queue).to(exchange).with("key1"); } //************************以下是关于备份的****************************** // 声明备份 Exchange @Bean("backupExchange") public FanoutExchange backupExchange() { return new FanoutExchange(BACKUP_EXCHANGE_NAME); } // 声明确认 Exchange 交换机的备份交换机 @Bean("confirmExchange") public DirectExchange confirmExchange() { ExchangeBuilder exchangeBuilder = ExchangeBuilder.directExchange(CONFIRM_EXCHANGE_NAME) .durable(true) //设置该交换机的备份交换机 .withArgument("alternate-exchange", BACKUP_EXCHANGE_NAME); return exchangeBuilder.build(); } // 声明警告队列 @Bean("warningQueue") public Queue warningQueue() { return QueueBuilder.durable(WARNING_QUEUE_NAME).build(); } // 声明报警队列绑定关系 @Bean public Binding warningBinding(@Qualifier("warningQueue") Queue queue, @Qualifier("backupExchange") FanoutExchange backupExchange) { return BindingBuilder.bind(queue).to(backupExchange); } // 声明备份队列 @Bean("backQueue") public Queue backQueue() { return QueueBuilder.durable(BACKUP_QUEUE_NAME).build(); } // 声明备份队列绑定关系 @Bean public Binding backupBinding(@Qualifier("backQueue") Queue queue, @Qualifier("backupExchange") FanoutExchange backupExchange) { return BindingBuilder.bind(queue).to(backupExchange); } }
@Component
@Slf4j
public class WarningConsumer {
private static final Logger logger = LoggerFactory.getLogger(WarningConsumer.class);
public static final String WARNING_QUEUE_NAME = "warning.queue";
@RabbitListener(queues = WARNING_QUEUE_NAME)
public void receiveWarningMsg(Message message) {
String msg = new String(message.getBody());
logger.error(WARNING_QUEUE_NAME + "队列,报警发现不可路由消息:{}", msg);
}
}
@Component
@Slf4j
public class BackUpConsumer {
private static final Logger logger = LoggerFactory.getLogger(BackUpConsumer.class);
public static final String WARNING_QUEUE_NAME = "backup.queue";
@RabbitListener(queues = WARNING_QUEUE_NAME)
public void receiveWarningMsg(Message message) {
String msg = new String(message.getBody());
logger.error(WARNING_QUEUE_NAME + "队列,备份发现不可路由消息:{}", msg);
}
}
在海量订单生成的业务高峰期,生产端有可能就会重复发生了消息,这时候消费端就要实现幂等性, 这就意味着我们的消息永远不会被消费多次,即使我们收到了一样的消息。
业界主流的幂等性有两种操作:a. 唯一 ID+指纹码机制,利用数据库主键去重, b.利用 redis 的原子性去实现
唯一ID+指纹码机制
指纹码:我们的一些规则或者时间戳加别的服务给到的唯一信息码,它并不一定是我们系统生成的,基本都是由我们的业务规则拼接而来,但是一定要保证唯一性,然后就利用查询语句进行判断这个 id 是否存在数据库中,优势就是实现简单就一个拼接,然后查询判断是否重复;劣势就是在高并发时,如果是单个数据库就会有写入性能瓶颈当然也可以采用分库分表提升性能,但也不是我们最推荐的方式。
note Redis 原子性
利用 redis 执行 setnx 命令,天然具有幂等性。从而实现不重复消费
-代码添加
Map<String, Object> params = new HashMap();
params.put("x-max-priority", 10);
channel.queueDeclare("hello", true, false, false, params);
AMQP.BasicProperties properties = new AMQP.BasicProperties().builder().priority(10).build();
public class Producer { private final static String QUEUE_NAME = "hello"; public static void main(String[] args) throws IOException, TimeoutException { // 创建一个连接工厂 ConnectionFactory connectionFactory = new ConnectionFactory(); // 设置配置 connectionFactory.setHost("192.168.56.10"); connectionFactory.setUsername("guest"); connectionFactory.setPassword("guest"); // 创建连接 Connection connection = connectionFactory.newConnection(); //获取信道 Channel channel = connection.createChannel(); /** * 生成一个队列 * 1.队列名称 * 2.队列里面的消息是否持久化 也就是是否用完就删除 * 3.该队列是否只供一个消费者进行消费 是否进行共享 true 可以多个消费者消费 * 4.是否自动删除 最后一个消费者端开连接以后 该队列是否自动删除 true 自动删除 * 5.其他参数 */ // channel.queueDeclare(QUEUE_NAME, false, false, false, null); // // String message = "hello world _ zgc"; // // /** // * 发送一个消息 // * 1.发送到那个交换机 // * 2.路由的 key 是哪个 // * 3.其他的参数信息 // * 4.发送消息的消息体 // */ // // channel.basicPublish("", QUEUE_NAME, null, message.getBytes()); // System.out.println("消息发送完毕"); // 声明队列时,设置优先级 Map<String, Object> params = new HashMap(); params.put("x-max-priority", 10); channel.queueDeclare("hello", true, false, false, params); // 给消息赋予一个 priority 属性 AMQP.BasicProperties properties = new AMQP.BasicProperties().builder().priority(10).build(); for (int i = 1; i < 11; i++) { String message = "info" + i; if (i == 5) { channel.basicPublish("", QUEUE_NAME, properties, message.getBytes()); } else { channel.basicPublish("", QUEUE_NAME, null, message.getBytes()); } System.out.println("发送消息完成:" + message); } }
RabbitMQ 从 3.6.0 版本开始引入了惰性队列的概念。惰性队列会尽可能的将消息存入磁盘中,而在消费者消费到相应的消息时才会被加载到内存中,它的一个重要的设计目标是能够支持更长的队列,即支持更多的消息存储。当消费者由于各种各样的原因(比如消费者下线、宕机亦或者是由于维护而关闭等)而致使长时间内不能消费消息造成堆积时,惰性队列就很有必要了。
默认情况下,当生产者将消息发送到 RabbitMQ 的时候,队列中的消息会尽可能的存储在内存之中, 这样可以更加快速的将消息发送给消费者。即使是持久化的消息,在被写入磁盘的同时也会在内存中驻留一份备份。当RabbitMQ 需要释放内存的时候,会将内存中的消息换页至磁盘中,这个操作会耗费较长的时间,也会阻塞队列的操作,进而无法接收新的消息。虽然 RabbitMQ 的开发者们一直在升级相关的算法, 但是效果始终不太理想,尤其是在消息量特别大的时候。
队列具备两种模式:default 和 lazy。默认的为default 模式,在3.6.0 之前的版本无需做任何变更。lazy 模式即为惰性队列的模式,可以通过调用 channel.queueDeclare 方法的时候在参数中设置,也可以通过 Policy 的方式设置,如果一个队列同时使用这两种方式设置的话,那么 Policy 的方式具备更高的优先级。 如果要通过声明的方式改变已有队列的模式的话,那么只能先删除队列,然后再重新声明一个新的。
在队列声明的时候可以通过“x-queue-mode”参数来设置队列的模式,取值为“default”和“lazy”。下面示例中演示了一个惰性队列的声明细节:
Map<String, Object> args = new HashMap<String, Object>();
args.put("x-queue-mode", "lazy");
channel.queueDeclare("myqueue", false, false, false, args);
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。