RabbitMQ
Hello RabbitMQ
- Maven依赖
<!--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>
- 生产者发送消息
/**
* 生产者 发消息
*/
public class Producer {
//队列名称
public static final String QUEUE_NAME = "hello";
//发消息
public static void main(String[] args) throws IOException, TimeoutException {
//创建一个连接工厂
ConnectionFactory factory = new ConnectionFactory();
//工厂ip
factory.setHost("127.0.0.1");
//用户名
factory.setUsername("guest");
//密码
factory.setPassword("guest");
//创建连接
Connection connection = factory.newConnection();
//获取信道
Channel channel = connection.createChannel();
/**
* 使用信道申明一个队列
* 参数1: 队列名
* 参数2: 队列持久化
* 参数3: 是否进行消息共享 true可以多个消费者进行消费,false只能一个消费者消费
* 参数4: 是否自动删除,最后一个消费者断开连接,该队列是否自动删除
* 参数5: 其他参数
*/
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
//消息
String message = "hello rabbitMQ";
/**
* 发送消息
* 参数1: 发送到那个交换机
* 参数2: 路由的key值是那个 本次是队列的名称
* 参数3: 其他参数信息 MessageProperties.PERSISTENT_TEXT_PLAIN:消息持久化
* 参数4: 发送的消息体
*/
channel.basicPublish("",QUEUE_NAME,null,message.getBytes());
System.out.println("消息发送完成!");
}
}
- 消费者消费消息
/**
* 消费者 接受消息
*/
public class Consumer {
//队列名称
public static final String QUEUE_NAME = "hello";
//接受消息
public static void main(String[] args) throws IOException, TimeoutException {
//创建连接工厂
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("127.0.0.1");
factory.setUsername("guest");
factory.setPassword("guest");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
//声明 消费成功的回调
DeliverCallback deliverCallback = (consumerTag,message) ->{
System.out.println("消费者接受到的消息:"+new String(message.getBody()));
};
//声明 取消消费的回调
CancelCallback cancelCallback = (consumerTag) ->{
System.out.println("消费消息被中断");
};
/**
* 消费消息
* 参数1: 消费那个队列
* 参数2: 消费成功之后是否要自动应答
* 参数3: 消费成功的回调
* 参数4: 取消消费的回调
*/
channel.basicConsume(QUEUE_NAME,true,deliverCallback,cancelCallback);
}
}
消息应答
- RabbitMQ一旦向消费者传递了一条消息,便立即将该消息柏标为删除。在这种情况下,突然有个消费者挂掉了,将丢失正在处理的消息。以及后续发送给该消费者的消息,因此它无法接收到。
- 为了保证消息在发送过程中不丢失,RabbitMQ引入了应答机制,消息应答就是消费者在接受到消息并且处理了该消息之后,高度RabbitMQ它已经处理了,RabbitMQ可以把该消息删除了。
- 自动应答
- 消息发送后立即被认为已经传送成功,这种模式需要在高吞吐量和数据传输安全性方面做权衡,因为这种模式如果消息在接收到之前,消费者那边出现连接或者 channel 关闭,那么消息就丢失了,当然另一方面这种模式消费者那边可以传递过载的消息,没有对传递的消息数量进行限制,当然这样有可能使得消费者这边由于接收太多还来不及处理的消息,导致这些消息的积压,最终使得内存耗尽,最终这些消费者线程被操作系统杀 死,所以这种模式仅适用在消费者可以高效并以某种速率能够处理这些消息的情况下使用。
- 手动应答
- Channel.basicAck(用于手动确认)
- Channel.basicNack(用于否定确认)
- channel.basicReject(用于否定确认)
- 与Channel.basicNack相比少了一个Multiple(批量应答)参数
//声明 消费成功的回调
DeliverCallback deliverCallback = (consumerTag,message) ->{
System.out.println("消费者接受到的消息:"+new String(message.getBody()));
//手动应答
/**
* 参数1 消息标记tag
* 参数2 是否批量应答
*/
channel.basicAck(message.getEnvelope().getDeliveryTag(),false);
};
/**
* 消费消息
* 参数1: 消费那个队列
* 参数2: 消费成功之后是否要自动应答 false手动应答
* 参数3: 消费成功的回调
* 参数4: 取消消费的回调
*/
channel.basicConsume(QUEUE_NAME,false,deliverCallback,cancelCallback);
- 消息自动重新入队
- 如果消费者由于某些原因失去连接(其通道已关闭,连接已关闭或TCP连接丢失),导致消息未发送ACK确认,RabbitMQ将了解到消息为完全处理,并将对其重新排队。如果此时其他消费者可以处理,它将很快将其重新分发给另一个消费者。这样,即使某个消费者偶尔死亡,也可以确保不会丢失任何消息。
- 不公平分发
- 轮训分发在某种场景下并不是很好,如果多个消费者的处理效率不一样,采用轮训分发消费速率高的消费者很大一部分时间处于空闲状态,可以设置channel.basicQos(1)改为不公平分发
//使用消费者信道设置为不公平分发 预期值为1
channel.basicQos(1);
发布确认
- 生产者将信道设置成confirm模式,一旦信道进入confirm模式,所有在该信道上面发布的消息都将会指派一个唯一的ID(从1开始),一旦消息被投递到所匹配的队列之后,broker就会发送一个确认给生产者(包括消息的唯一ID),这就使得生产者直到消息已经正确到达目的队列了。
- 如果消息和队列是可持久化,那么确认消息会在将消息写入磁盘之后发出,broker回传给生产者的确认消息中delivery-tag域包含了确认消息的序列号,此外broker也可以设置basic.ack的mulitple域,表示到这个序列号之前的所有消息都已经得到处理了。
- confirm模式最大的好处在于它是异步的,一旦发布一条消息,生产者应用程序就可以在等信道返回确认的同时继续发送下一条消息,当消息最终得到确认之后,生产者应用便可以通过回调方法来处理该确认消息,如果RabbitMQ因为自身内部原因导致消息丢失,就会发送一条nack消息,生产者应用程序同样可以在回调方法中处理该nack消息。
- 单个确认发布
//开启发布确认
channel.confirmSelect();
for(int i =0;i<10;i++){
//发送消息
channel.basicPublish("",QUEUE_NAME, MessageProperties.PERSISTENT_TEXT_PLAIN,message.getBytes());
//单个消息进行发布确认
boolean flag = channel.waitForConfirms();
if (flag){
System.out.println("消息发送成功");
}
}
- 批量确认发布
- 先发布一批消息然后一起确认可以极大地提高吞吐量,当发生故障导致发布问题出现时,不知道是哪个消息出现问题了
//开启发布确认
channel.confirmSelect();
for(int i =0;i<10;i++){
//发送消息
channel.basicPublish("",QUEUE_NAME, MessageProperties.PERSISTENT_TEXT_PLAIN,message.getBytes());
//批量发布确认
if (i % 5 == 0){
boolean flag = channel.waitForConfirms();
if (flag){
System.out.println("消息发送成功");
}
}
}
- 异步确认发布
//开启发布确认
channel.confirmSelect();
//消息确认成功回调
/**
* 参数1:消息的标识
* 参数2:是否为批量确认
*/
ConfirmCallback ackCallback = (deliveryTag,multiple)->{
System.out.println("确认的消息:"+deliveryTag);
};
//消息确认失败回调
ConfirmCallback nackCallback = (deliveryTag,multiple)->{
System.out.println("未确认的消息:"+deliveryTag);
};
channel.addConfirmListener(ackCallback,nackCallback);
for(int i =0;i<10;i++){
//发送消息
Thread.sleep(1000);
channel.basicPublish("",QUEUE_NAME, MessageProperties.PERSISTENT_TEXT_PLAIN,message.getBytes());
}
交换机
- Fanout(发布订阅、广播、扇出)
/**
* 消费者1
*/
public class Consumer1 {
public static void main(String[] args) throws IOException {
//声明信道
Channel channel = RabbitmqUtils.getChannel();
//声明交换机 fanout类型交换机
channel.exchangeDeclare("logs","fanout");
/**
* 声明一个队列 临时队列
* 临时队列名称是随机的,当消费者断开与队列的连接的时候,队列自动删除
*/
String queueName = channel.queueDeclare().getQueue();
//绑定交换机
channel.queueBind(queueName,"logs","");
DeliverCallback deliverCallback = (consumerTag,message)->{
System.out.println("消费者1接收到的消息:"+new String(message.getBody()));
};
//消费者消费消息
channel.basicConsume(queueName,true,deliverCallback,consumerTag->{});
}
}
/**
* 消费者2
*/
public class Consumer2 {
public static void main(String[] args) throws IOException {
//声明信道
Channel channel = RabbitmqUtils.getChannel();
//声明交换机 fanout类型交换机
channel.exchangeDeclare("logs","fanout");
/**
* 声明一个队列 临时队列
* 临时队列名称是随机的,当消费者断开与队列的连接的时候,队列自动删除
*/
String queueName = channel.queueDeclare().getQueue();
//绑定交换机
channel.queueBind(queueName,"logs","");
DeliverCallback deliverCallback = (consumerTag,message)->{
System.out.println("消费者2接收到的消息:"+new String(message.getBody()));
};
//消费者消费消息
channel.basicConsume(queueName,true,deliverCallback,consumerTag->{});
}
}
/**
* 生产者
*/
public class Provider {
public static void main(String[] args) throws IOException {
Channel channel = RabbitmqUtils.getChannel();
for (int i = 0;i<10;i++){
String message = "消息"+i;
channel.basicPublish("logs","",null,message.getBytes());
}
}
}
-
Direct(直接交换机)
-
消费者1,将队列绑定到交换机,routingKey为info1、info2
/**
* 消费者1
*/
public class Consumer1 {
public static void main(String[] args) throws IOException {
//声明信道
Channel channel = RabbitmqUtils.getChannel();
//声明交换机 Direct类型交换机
channel.exchangeDeclare("direct_log", BuiltinExchangeType.DIRECT);
/**
* 声明一个队列 临时队列
* 临时队列名称是随机的,当消费者断开与队列的连接的时候,队列自动删除
*/
String queueName = channel.queueDeclare().getQueue();
//绑定交换机 该队列的routingKey为info 可以绑定多个
channel.queueBind(queueName,"direct_log","info1");
channel.queueBind(queueName,"direct_log","info2");
DeliverCallback deliverCallback = (consumerTag, message)->{
System.out.println("消费者1接收到的消息:"+new String(message.getBody()));
};
//消费者消费消息
channel.basicConsume(queueName,true,deliverCallback,consumerTag->{});
}
}
- 消费者2,绑定了一个队列到交换机,routingkey为info3
public class Consumer2 {
public static void main(String[] args) throws IOException {
//声明信道
Channel channel = RabbitmqUtils.getChannel();
//声明交换机 Direct类型交换机
channel.exchangeDeclare("direct_log", BuiltinExchangeType.DIRECT);
/**
* 声明一个队列 临时队列
* 临时队列名称是随机的,当消费者断开与队列的连接的时候,队列自动删除
*/
String queueName = channel.queueDeclare().getQueue();
//绑定交换机 该队列的routingKey为info 可以绑定多个
channel.queueBind(queueName,"direct_log","info3");
DeliverCallback deliverCallback = (consumerTag, message)->{
System.out.println("消费者2接收到的消息:"+new String(message.getBody()));
};
//消费者消费消息
channel.basicConsume(queueName,true,deliverCallback,consumerTag->{});
}
}
- 生产者
/**
* 生产者
*/
public class Provider {
public static void main(String[] args) throws IOException {
Channel channel = RabbitmqUtils.getChannel();
String message = "消息";
//发送消息设置routingkey为info1,则routingkey为info1的队列才会收到该条消息
channel.basicPublish("direct_log","info1",null,message.getBytes());
//发送消息设置routingkey为info2,则routingkey为info2的队列才会收到该条消息
channel.basicPublish("direct_log","info3",null,message.getBytes());
}
}
- Topic(主题模式)
- 可以使用表达式来设置routingkey
- *(星号)可以代替一个单词
- #(井号)可以替代另个或多个单词
- 消费者1,routingkey为.orange.
/**
* 消费者1
*/
public class Consumer1 {
public static void main(String[] args) throws IOException {
Channel channel = RabbitmqUtils.getChannel();
//声明topic类型的交换机
channel.exchangeDeclare("topic_Logs","topic");
//声明队列
channel.queueDeclare("queue01",false,false,false,null);
//绑定交换机
channel.queueBind("queue01","topic_Logs","*.orange.*");
//接收消息
DeliverCallback deliverCallback = ((consumerTag, message) -> {
System.out.println("接收到的消息:"+ new String(message.getBody()));
});
channel.basicConsume("queue01",true,deliverCallback,consumerTag -> {});
}
}
- 消费者2,routingkey为laz.y.#
public class Consumer2 {
public static void main(String[] args) throws IOException {
Channel channel = RabbitmqUtils.getChannel();
//声明topic类型的交换机
channel.exchangeDeclare("topic_Logs","topic");
//声明队列
channel.queueDeclare("queue02",false,false,false,null);
//绑定交换机
channel.queueBind("queue02","topic_Logs","laz.y.#");
//接收消息
DeliverCallback deliverCallback = ((consumerTag, message) -> {
System.out.println("接收到的消息:"+ new String(message.getBody()));
});
channel.basicConsume("queue02",true,deliverCallback,consumerTag -> {});
}
}
- 生产者分别给两个路由发送一条消息
public class Provider {
public static void main(String[] args) throws IOException {
Channel channel = RabbitmqUtils.getChannel();
//路由到 routingkey为*.orange.*的队列
channel.basicPublish("topic_Logs","test.orange.test",null,"消息,test.orange.test".getBytes());
//路由到 routingkey为laz.y.#的队列
channel.basicPublish("topic_Logs","laz.y.test",null,"消息,laz.y.test".getBytes());
}
}
死信队列
- 私信队列,无法被消费的消息,生产者叫消息投递到rabbitmq,消费者从队列中取出消息进行消费,但某些时候由于特定的原因导致队列中的某些消息无法被消费,这样的消息如果没有后序处理,就变成了死信,有死信自然就有了死信队列
- 应用场景,为了保证订单业务的消息数据不丢失,需要使用到RabbitMQ的死信队列机制,当消息消费发生异常时,将消息投入死信队列中。
- 死信的来源
- 消息TTL(存活时间)过期
- 队列达到最大长度
- 消息被拒绝,并且requeue=false(不再放回队列中)
- 消费者1
public class Consumer1 {
//普通交换机的名称
public static final String NORMAL_EXCHANGE = "normal_exchange";
//死信交换机名称
public static final String DEAD_EXCHANGE = "dead_exchange";
public static void main(String[] args) throws IOException {
Channel channel = RabbitmqUtils.getChannel();
//声明死信和普通交换机
channel.exchangeDeclare(NORMAL_EXCHANGE, BuiltinExchangeType.DIRECT);
channel.exchangeDeclare(DEAD_EXCHANGE,BuiltinExchangeType.DIRECT);
//声明死信队列
String deadQueue = "dead-queue";
channel.queueDeclare(deadQueue,false,false,false,null);
//死信队列绑定死信交换机
channel.queueBind(deadQueue,DEAD_EXCHANGE,"lisi");
//正常队列绑定死信交换机
Map<String,Object> params = new HashMap<>();
params.put("x-dead-letter-exchange",DEAD_EXCHANGE);
params.put("x-dead-letter-routing-key","lisi");
//正常队列设置最大长度,超过最大长度的消息会被发送到死信交换机中
params.put("x-max-length",10);
//声明正常队列并设置死信交换机和死信队列
String normalQueue = "normal-queue";
channel.queueDeclare(normalQueue,false,false,false,params);
channel.queueBind(normalQueue,NORMAL_EXCHANGE,"zhangsan");
//接收消息
DeliverCallback deliverCallback = (consumerTag, message) -> {
String msg = new String(message.getBody(),"UTF-8");
if (msg.equals("info5")){
System.out.println("Consumer1被拒绝的消息:"+msg);
//被拒绝的消息会被放入死信队列中
channel.basicReject(message.getEnvelope().getDeliveryTag(),false);
}else{
System.out.println("Consumer1消费的消息");
channel.basicAck(message.getEnvelope().getDeliveryTag(),false);
}
};
channel.basicConsume(normalQueue,false,deliverCallback,consumerTag->{});
}
}
- 消费者2
public class Consumer2 {
//死信交换机名称
public static final String DEAD_EXCHANGE = "dead_exchange";
public static void main(String[] args) throws IOException {
Channel channel = RabbitmqUtils.getChannel();
channel.exchangeDeclare(DEAD_EXCHANGE, BuiltinExchangeType.DIRECT);
String deadQueue = "dead-queue";
channel.queueDeclare(deadQueue,false,false,false,null);
channel.queueBind(deadQueue,DEAD_EXCHANGE,"lisi");
DeliverCallback deliverCallback = ((consumerTag, message) -> {
System.out.println("Consumer2接收到的消息:"+message.getBody());
});
channel.basicConsume(deadQueue,true,deliverCallback,consumerTag -> {});
}
}
- 生产者
public class Producer {
private static final String NORMAL_EXCHANGE = "normal_exchange";
public static void main(String[] argv) throws Exception {
try (Channel channel = RabbitmqUtils.getChannel()) {
channel.exchangeDeclare(NORMAL_EXCHANGE, BuiltinExchangeType.DIRECT);
//设置消息的 TTL 时间
AMQP.BasicProperties properties = new AMQP.BasicProperties().builder().expiration("10000").build();
for (int i = 1; i <11 ; i++) {
String message="info"+i;
channel.basicPublish(NORMAL_EXCHANGE, "zhangsan", properties, message.getBytes());
System.out.println("生产者发送消息:"+message);
}
}
}
}
延迟队列
-
整合SpringBoot
-
maven依赖
<!--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>
- application.properties
spring.rabbitmq.host=127.0.0.1
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
- 队列TTL
- 创建两个队列QA和QB,两者队列TTL分别设置为10s和40s,然后再创建一个交换机X和死信交换机Y,他们的类型都是direct,创建一个死信队列QD,他们的绑定关系如下:
- 声明交换机和队列
@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";
// 声明 xExchange (普通交换机)
@Bean("xExchange")
public DirectExchange xExchange(){
return new DirectExchange(X_EXCHANGE);
}
// 声明 xExchange (死信交换机)
@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<>(3);
//声明当前队列绑定的死信交换机
args.put("x-dead-letter-exchange", Y_DEAD_LETTER_EXCHANGE);
//声明当前队列的死信路由 key
args.put("x-dead-letter-routing-key", "YD");
//声明队列的 TTL
args.put("x-message-ttl", 10000);
return QueueBuilder.durable(QUEUE_A).withArguments(args).build();
}
// 声明队列 A 绑定 X 交换机
@Bean
public Binding queueaBindingX(@Qualifier("queueA") Queue queueA,
@Qualifier("xExchange") DirectExchange xExchange){
return BindingBuilder.bind(queueA).to(xExchange).with("XA");
}
//声明队列 B ttl 为 40s 并绑定到对应的死信交换机
@Bean("queueB")
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
args.put("x-message-ttl", 40000);
return QueueBuilder.durable(QUEUE_B).withArguments(args).build();
}
//声明队列 B 绑定 X 交换机
@Bean
public Binding queuebBindingX(@Qualifier("queueB") Queue queue1B,
@Qualifier("xExchange") DirectExchange xExchange){
return BindingBuilder.bind(queue1B).to(xExchange).with("XB");
}
//声明死信队列 QD
@Bean("queueD")
public Queue queueD(){
return new Queue(DEAD_LETTER_QUEUE);
}
//声明死信队列 QD 绑定关系
@Bean
public Binding deadLetterBindingQAD(@Qualifier("queueD") Queue queueD,
@Qualifier("yExchange") DirectExchange yExchange){
return BindingBuilder.bind(queueD).to(yExchange).with("YD");
}
}
- 生产者向X交换机发送两条消息,routingkey分别为XA、XB,消息过期时间分别为10s、40s
@Slf4j
@RequestMapping("ttl")
@RestController
public class SendMsgController {
@Autowired
private RabbitTemplate rabbitTemplate;
@GetMapping("sendMsg/{message}")
public void sendMsg(@PathVariable String message){
log.info("当前时间:{},发送一条信息给两个 TTL 队列:{}", new Date(), message);
rabbitTemplate.convertAndSend("X", "XA", "消息来自 ttl 为 10S 的队列: "+message);
rabbitTemplate.convertAndSend("X", "XB", "消息来自 ttl 为 40S 的队列: "+message);
}
}
- 消费者消费死信队列QD中的消息,生产者发送消息给交换机X的XA队列和XB队列,当消息时间超过设置的存活时间,则消息会被发送到死信交换机,然后由死信队列QD消费到
@Slf4j
@Component
public class DeadLetterQueueConsumer {
//接收死信队列QD中的消息
@RabbitListener(queues = "QD")
public void receiveD(Message message, Channel channel) throws IOException {
String msg = new String(message.getBody());
log.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;
});
log.info("当前时间:{},发送一条时长{}毫秒 TTL 信息给队列 C:{}", new Date(),ttlTime, message);
}
- 使用消息TTL时间+死信队列实现延迟对哦,存在问题:消息可能并不会按时死亡,因为RabbitMQ只会检查第一条消息是否过期,如果过期则丢到死信度列,如果第一个消息的延迟时长很长,而第二个消息的延迟时间很短,第二个消息并不会优先得到执行。则需要使用RabbitMQ延迟队列插件实现。
发布确认高级
- 确认交换机
- 声明交换机和队列,并绑定交换机和队列
@Configuration
public class ConfirmConfig {
//交换机
public static final String confirm_exchange_name = "confirm_exchange";
//队列
public static final String confirm_queue_name = "confirm_queue";
//RoutingKey
public static final String confirm_routing_key = "key1";
//声明交换机
@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 queueBindExchange(@Qualifier("confirmQueue") Queue confirmQueue,
@Qualifier("confirmExchange") DirectExchange confirmExchange){
return BindingBuilder.bind(confirmQueue).to(confirmExchange).with(confirm_routing_key);
}
}
- 生产者,发送消息
@RestController
@RequestMapping("/confirm")
@Slf4j
public class ProducerController {
@Autowired
private RabbitTemplate rabbitTemplate;
//发消息
@GetMapping("/sendMessage/{message}")
public void sendMessage(@PathVariable("message") String message){
rabbitTemplate.convertAndSend(ConfirmConfig.confirm_exchange_name,
ConfirmConfig.confirm_routing_key,message);
log.info("发送消息:" + message);
}
}
- 消费者,接收消息
/**
* 消费者接收消息
*/
@Component
@Slf4j
public class consumer {
@RabbitListener(queues = ConfirmConfig.confirm_queue_name)
public void receiveMsg(Message message){
log.info("消费者接收到的消息:{}",new String(message.getBody()));
}
}
- 请求控制器地址发送消息
- 生产者成功发送消息,消费者成功接收到消息
- 配置文件添加交换机发布确认配置
#交换机发布确认
# none:禁用发布确认,默认
# correlated:发布消息成功到交换机后会触发回调方法
# simple
spring.rabbitmq.publisher-confirm-type=correlated
- 实现交换机确认回调接收
/**
* 回调接口
*/
@Component
@Slf4j
public class MyCallBack implements RabbitTemplate.ConfirmCallback {
@Autowired
private RabbitTemplate rabbitTemplate;
@PostConstruct
public void init(){
//注入到RabbitTemplate
rabbitTemplate.setConfirmCallback(this);
}
/**
* 交换机确认回调方法
* @param correlationData 保存回调消息的ID及相关信息
* @param ack 交换机接收成功 true,交换机接收失败 false
* @param cause 交换机接收成功 null,交换机接收失败 失败的原因
*/
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
if (ack){
log.info("交换机成功接收到消息");
}else {
log.info("交换机未收到消息");
}
}
}
- 发送消息是加上CorrelationData对象参数
@RestController
@RequestMapping("/confirm")
@Slf4j
public class ProducerController {
@Autowired
private RabbitTemplate rabbitTemplate;
//发消息
@GetMapping("/sendMessage/{message}")
public void sendMessage(@PathVariable("message") String message){
CorrelationData correlationData = new CorrelationData("001");
rabbitTemplate.convertAndSend(ConfirmConfig.confirm_exchange_name,
ConfirmConfig.confirm_routing_key,message,correlationData);
log.info("发送消息:" + message);
}
}
- 发送消息后,触发回调接口(交换机成功接收到消息)
- 发送消息的时候填上没有的交换机名(error_exchange)
@GetMapping("/sendMessage/{message}")
public void sendMessage(@PathVariable("message") String message){
CorrelationData correlationData = new CorrelationData("001");
rabbitTemplate.convertAndSend("error_exchange",
ConfirmConfig.confirm_routing_key,message,correlationData);
log.info("发送消息:" + message);
}
- 消息发送后,触发回调接口(交换机未接收到消息)
- 回退消息
- 在仅开启了生产者确认机制的情况下,交换机接收到消息后,会直接给消息生产者发送确认消息,如果发现该消息不可路由,那么消息会被直接丢弃,此时生产者不知道消息被丢弃这件事情的。
- 通过设置mandatory参数可以在当消息传递过程中不可达目的地时将消息返回给生产者
# 发布推回
spring.rabbitmq.publisher-returns=true
- 回调类实现RabbitTemplate.ReturnCallback接口
/**
* 回调接口
*/
@Component
@Slf4j
public class MyCallBack implements RabbitTemplate.ConfirmCallback,RabbitTemplate.ReturnCallback {
@Autowired
private RabbitTemplate rabbitTemplate;
@PostConstruct
public void init(){
//注入到RabbitTemplate
rabbitTemplate.setConfirmCallback(this);
rabbitTemplate.setReturnCallback(this);
}
/**
* 可以在当消息传递过程中不可达目的地时将消息返回给生产者
* message: 消息
* exchange 交换机
* replyText 原因
* routingKey 路由key
*/
@Override
public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
log.error("消息{},被交换机{}推回,推回原因:{},routingkey: {}",
new String(message.getBody()),exchange,replyText,routingKey);
}
}
- 生产者发送消息给交换机,设置一个错误的routingKey
@RestController
@RequestMapping("/confirm")
@Slf4j
public class ProducerController {
@Autowired
private RabbitTemplate rabbitTemplate;
//发消息
@GetMapping("/sendMessage/{message}")
public void sendMessage(@PathVariable("message") String message){
CorrelationData correlationData = new CorrelationData("001");
rabbitTemplate.convertAndSend(ConfirmConfig.confirm_exchange_name,
"error_routingKey",message,correlationData);
log.info("发送消息:" + message);
}
}
- 生产者发送消息,交换机成功收到调用了确认回调方法,消息不可达目的队列则调用了消息返回接口
备份交换机
- 当我们为某一个交换机声明一个对应的备份交换机时,当交换机接收到一条不可路由的消息时,将会把这条消息转发到备份交换机中,由备份交换机来进行转发和处理,通常备份交换机的类型为Fanout,这样就能把所有消息都投递到于其绑定的队列中。还可以建立一个报警队列,用独立的消费者来进行检测和报警。
- 增加一个备份交换机和一个备份队列,一个报警队列
/**
* 发布确认
*/
@Configuration
public class ConfirmConfig {
//交换机
public static final String confirm_exchange_name = "confirm_exchange";
//队列
public static final String confirm_queue_name = "confirm_queue";
//RoutingKey
public static final String confirm_routing_key = "key1";
//备份交换机
public static final String backup_exchange_name = "backup_exchange";
//备份队列
public static final String backup_queue_name = "backup_queue";
//报警队列
public static final String warning_queue_nam = "warning_queue";
//声明交换机并绑定备份交换机
@Bean("confirmExchange")
public DirectExchange confirmExchange(){
return ExchangeBuilder.directExchange(confirm_exchange_name).durable(true)
.withArgument("alternate-exchange",backup_exchange_name).build();
}
//声明队列
@Bean("confirmQueue")
public Queue confirmQueue(){
return QueueBuilder.durable(confirm_queue_name).build();
}
//绑定交换机和队列
@Bean
public Binding queueBindExchange(@Qualifier("confirmQueue") Queue confirmQueue,
@Qualifier("confirmExchange") DirectExchange confirmExchange){
return BindingBuilder.bind(confirmQueue).to(confirmExchange).with(confirm_routing_key);
}
//备份交换机
@Bean("backupExchange")
public FanoutExchange backupExchange(){
return new FanoutExchange(backup_exchange_name);
}
//备份队列
@Bean("backQueue")
public Queue backQueue(){
return QueueBuilder.durable(backup_queue_name).build();
}
//报警队列
@Bean
public Queue warningQueue(){
return QueueBuilder.durable(warning_queue_nam).build();
}
//绑定备份队列到备份交换机
@Bean
public Binding backQueueBindBackExchange(@Qualifier("backQueue") Queue backQueue,
@Qualifier("backupExchange") FanoutExchange backupExchange){
return BindingBuilder.bind(backQueue).to(backupExchange);
}
//绑定报警队列到备份交换机
@Bean
public Binding warningQueueBindBackExchange(@Qualifier("warningQueue") Queue warningQueue,
@Qualifier("backupExchange") FanoutExchange backupExchange){
return BindingBuilder.bind(warningQueue).to(backupExchange);
}
}
- 新增两个消费者,分别消费备份队列和报警队列的消息
/**
* 消费者接收消息
*/
@Component
@Slf4j
public class consumer {
@RabbitListener(queues = ConfirmConfig.confirm_queue_name)
public void receiveMsg(Message message){
log.info("消费者接收到的消息:{}",new String(message.getBody()));
}
@RabbitListener(queues = ConfirmConfig.backup_queue_name)
public void backQueueConsumer(Message message){
log.info("消费者接收到备份队列的消息:{}",new String(message.getBody()));
}
@RabbitListener(queues = ConfirmConfig.warning_queue_nam)
public void warningQueueConsumer(Message message){
log.info("消费者接收到报警队列的消息:{}",new String(message.getBody()));
}
}
- 发送消息给交换机,指定一个不存在的routingkey则交换机会把消息发送给备份交换机,备份交换机再把消息发送给备份队列和报警队列
- mandatory参数与备份交换机一起使用的时候,备份交换机优先级高
RabbitMQ其他知识点
- 幂等性
- 概念 - 用户对于同一操作发起的一次请求或者多次请求的结果是一致的,不会因为多次点击而产生了副作用。举个最简单的例子,那就是支付,用户购买商品后支付,支付扣款成功,但是返回结果的时候网络异常,此时钱已经扣了,用户再次点击按钮,此时会进行第二次扣款,返回结果成功,用户查询余额发现多扣钱了,流水记录也变成了两条。在以前的单应用系统中,我们只需要把数据操作放入事务中即可,发生错误立即回滚,但是再响应客户端的时候也有可能出现网络中断或者异常等等 - 消息重复消费 - 消费者在消费 MQ 中的消息时,MQ 已把消息发送给消费者,消费者在给 MQ 返回 ack 时网络中断,故 MQ 未收到确认信息,该条消息会重新发给其他的消费者,或者在网络重连后再次发送给该消费者,但实际上该消费者已成功消费了该条消息,造成消费者消费了重复的消息。 - 解决思路 - MQ 消费者的幂等性的解决一般使用全局 ID 或者写个唯一标识比如时间戳 或者 UUID 或者订单消费者消费 MQ 中的消息也可利用 MQ 的该 id 来判断,或者可按自己的规则生成一个全局唯一 id,每次消费消息时用该 id 先判断该消息是否已消费过。 - 消费端的幂等性保障 - 在海量订单生成的业务高峰期,生产端有可能就会重复发生了消息,这时候消费端就要实现幂等性,这就意味着我们的消息永远不会被消费多次,即使我们收到了一样的消息。业界主流的幂等性有两种操作:a.唯一 ID+指纹码机制,利用数据库主键去重, b.利用 redis 的原子性去实现 - 唯一ID+指纹码机制 - 指纹码:我们的一些规则或者时间戳加别的服务给到的唯一信息码,它并不一定是我们系统生成的,基本都是由我们的业务规则拼接而来,但是一定要保证唯一性,然后就利用查询语句进行判断这个 id 是否存在数据库中,优势就是实现简单就一个拼接,然后查询判断是否重复;劣势就是在高并发时,如果是单个数据库就会有写入性能瓶颈当然也可以采用分库分表提升性能,但也不是我们最推荐的方式。 - Redis原子性 - 利用 redis 执行 setnx 命令,天然具有幂等性。从而实现不重复消费
优先级队列
- 声明队列的时候设置队列的最大优先级0-255之间
Map<String,Object> arguments = new HashMap<>();
arguments.put("x-max-priority",10);//设置队列的最大优先级 0-255之间
channel.queueDeclare(QUEUE_NAME,false,false,false,arguments);
- 发送消息的时候设置第五条消息的优先级为5
for (int i = 0;i<11;i++){
String message = "info" + i;
//设置第五条消息的优先级为10
if (i== 5){
AMQP.BasicProperties properties = new AMQP.BasicProperties().builder().priority(5).build();
channel.basicPublish("",QUEUE_NAME,properties,message.getBytes());
}else{
channel.basicPublish("",QUEUE_NAME,null,message.getBytes());
}
}
- 生产者发送消息
- 等生产者发送消息到队列中,队列会将没消费的消息进行排序,优先级高的排在前面优先消费,等队列接收到所有的消息在启动消费者查看。第五条消息被优先消费
惰性队列
- 惰性队列会尽可能的将消息存入磁盘中,而在消
费者消费到相应的消息时才会被加载到内存中,它的一个重要的设计目标是能够支持更长的队列,即支持更多的消息存储。当消费者由于各种各样的原因(比如消费者下线、宕机亦或者是由于维护而关闭等)而致使长时间内不能消费消息造成堆积时,惰性队列就很有必要了。
//声明队列的时候指定为惰性队列
arguments.put("x-queue-mode","lazy");