【RabbitMQ】基于RabbitMQ来实现基本的消息生产与消费

在本文中直接进行介绍消息的生产和消费过程;
工程的开发基于Springboot和RabbitMQ
RabbitMQ交换机的模式为:Direct Mode


在进行消息生产和消费之前需要在SpringBoot工程的pom.xml文件中,引入如下所示的依赖:

		<!-- rabbitmq依赖 -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-amqp</artifactId>
		</dependency>
1、消息生产端
1.1、配置文件

在application.properties文件中进行相关的配置,具体配置内容如下:

server.port=8889

# RabbitMQ producer configuration
spring.rabbitmq.host=localhost
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
spring.rabbitmq.virtual-host=/
# set timeout
spring.rabbitmq.connection-timeout=15000 # 连接超时时间(单位:ms)0表示无穷大,不超时

# RabbitMQ 生产端消息确认机制设置
# 确认消息已经发送到队列中
spring.rabbitmq.publisher-returns=true
# 确认消息已经发送到交换机中
spring.rabbitmq.publisher-confirm-type=correlated

# 热部署
spring.devtools.restart.enabled=true
1.2、交换机、消息队列配置文件

在RabbitMQConfig.java文件中进行交换机、队列的初始化配置,如下所示:

@Configuration
public class RabbitMQConfig {
	private static final String EXCHANGE_NAME = "exchange_demo";
	private static final String QUEUE_NAME = "queue_demo";
	private static final String ROUTE_KEY = "routeKey_demo";
	@Bean
	public DirectExchange createDirectExchange() {
		return new DirectExchange(EXCHANGE_NAME, true, false);
	}
	//name:交换机名称
	//durable: 是否持久化
	//autoDelete: 有过消费者,并且所有的消费者都取消订阅则自动删除
	//DirectExchange(String name, boolean durable, boolean autoDelete)

	@Bean
	public Queue createQueue() {
		return new Queue(QUEUE_NAME, true, false, false, null);
	}
	// name: 队列名称
	// durable: 是否持久化
	// exclusive: 是否只能在本次的连接中访问,一般为false
	// autoDelete: 没有消费者的时候是否自动删除
	// arguments: 参数配置
	//Queue(String name, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments)

	@Bean
	public Binding createBinding() {
		return BindingBuilder.bind(createQueue()).to(createDirectExchange()).with(ROUTE_KEY);
	}
}
1.2.1 消息的持久化

如果要实现消息的持久化则exchangequeuemessage都要进行持久化,缺一不可,关于exchangequeue的持久化如上述的设置即可,至于message的持久化,可以来看一下源码:

rabbitTemplate.convertAndSend(...) 

// 来看一下convertAndSend的实现方法
@Override
public void convertAndSend(String exchange, String routingKey, final Object object,
			@Nullable CorrelationData correlationData) throws AmqpException {
	send(exchange, routingKey, convertMessageIfNecessary(object), correlationData);
}
// 看下convertMessageIfNecessary方法
protected Message convertMessageIfNecessary(final Object object) {
	if (object instanceof Message) {
		return (Message) object;
	}
	return getRequiredMessageConverter().toMessage(object, new MessageProperties());
}
// 接下来看下toMessage方法中的MessageProperties类对象参数,在MessageProperties类中有一个属性 
// MessageDeliveryMode DEFAULT_DELIVERY_MODE = MessageDeliveryMode.PERSISTENT;
// DEFAULT_DELIVERY_MODE默认的模式为持久化的,所以消息的持久化不需要用户来设置 
1.3、消息的发送

在service层中进行消息的发送操作:

@Service
public class MessageService implements IMessageService {
	@Autowired
	private RabbitTemplate rabbitTemplate;

	@Override
	public void sendMessage(Message message) {
		String msgId = UUID.randomUUID().toString();
		// CorrelationData :data to correlate publisher confirms. (在进行消息确认时必须要有)
		CorrelationData correlationData = new CorrelationData(msgId);
		rabbitTemplate.convertAndSend("exchange_demo", "routeKey_demo", message, correlationData);
		System.out.println("Send Message       :消息已经发出!");
	}
}
1.4、消息实体类
@Data
public class Message implements Serializable {
	private static final long serialVersionUID = 1L;

	private String id;
	private String msgData;
}
1.5、生产端消息确认

实现生产端的消息确认需要添加如下配置文件,在application.properties中的配置入1.1所示;

@Configuration
public class MessageAckConfig {

	@Autowired
	private ConnectionFactory connectionFactory;

	@Bean
	public RabbitTemplate createRabbitTemplate() {
		RabbitTemplate rabbitTemplate = new RabbitTemplate();
		rabbitTemplate.setConnectionFactory(connectionFactory);
		/**
		 * 当mandatory标志位设置为true时 如果exchange根据自身类型和消息routingKey无法找到一个合适的queue存储消息
		 * 那么broker会调用basic.return方法将消息返还给生产者 当mandatory设置为false时,出现上述情况broker会直接将消息丢弃
		 * (必须进行配置,否则不能调用setReturnCallback方法)
		 */
		rabbitTemplate.setMandatory(true);
		// 消息确认,一般消息成功与否都会触发该方法
		rabbitTemplate.setConfirmCallback(new ConfirmCallback() {
			@Override
			public void confirm(CorrelationData correlationData, boolean ack, String cause) {
				System.err.println("ConfirmCallback     相关数据:" + correlationData);
				System.err.println("ConfirmCallback     确认情况:" + ack);
				System.err.println("ConfirmCallback     原因:" + cause);
			}
		});
		// 消息已经推送到交换机但是找不到队列,就会触发ReturnCallback,进行消息的回退
		rabbitTemplate.setReturnCallback(new ReturnCallback() {
			@Override
			public void returnedMessage(Message message, int replyCode, String replyText, String exchange,
					String routingKey) {
				System.err.println("ReturnCallback:     " + "消息:" + message);
				System.err.println("ReturnCallback:     " + "回应码:" + replyCode);
				System.err.println("ReturnCallback:     " + "回应信息:" + replyText);
				System.err.println("ReturnCallback:     " + "交换机:" + exchange);
				System.err.println("ReturnCallback:     " + "路由键:" + routingKey);
			}
		});
		return rabbitTemplate;
	}
}

参考博客:https://blog.csdn.net/qq_35387940/article/details/100514134

消息成功发送到队列上:

ConfirmCallback     相关数据:CorrelationData [id=cf378f0b-2ad9-499d-bc21-a18e07e5db26]
ConfirmCallback     确认情况:true
ConfirmCallback     原因:null

消息发送到broker时,找到exchange,而没有找到queue:

ReturnCallback:     消息:(Body:'[B@1b6abdf6(byte[111])' MessageProperties [headers={spring_returned_message_correlation=fe5a3286-0a31-41b2-9dc2-5c4ba9bd2d6e}, contentType=application/x-java-serialized-object, contentLength=0, receivedDeliveryMode=PERSISTENT, priority=0, deliveryTag=0])
ReturnCallback:     回应码:312
ReturnCallback:     回应信息:NO_ROUTE
ReturnCallback:     交换机:new_exchange
ReturnCallback:     路由键:routeKey_demo

ConfirmCallback     相关数据:CorrelationData [id=fe5a3286-0a31-41b2-9dc2-5c4ba9bd2d6e]
ConfirmCallback     确认情况:true
ConfirmCallback     原因:null
1.6、死信队列

实现死信队列只需在队列初始化时,在参数中添加一些配置,然后初始化一个死信队列即可;对1.2中的配置文件进行更改:

@Configuration
public class RabbitMQConfig {
	private static final String EXCHANGE_NAME = "exchange_demo";
	private static final String QUEUE_NAME = "queue_demo";
	private static final String ROUTE_KEY = "routeKey_demo";
	// Dead Letter
	private static final String EXCHANGE_DL_NAME = "exchange_dl_timeout";
	private static final String QUEUE_DL_NAME = "queue_dl_timeout";
	private static final String ROUTE_KEY_DL = "routeKey_dl_timeout";
	@Bean
	public DirectExchange createDirectExchange() {
		return new DirectExchange(EXCHANGE_NAME, true, false);
	}

	@Bean
	public Queue createQueue() {
		Map<String, Object> map = new HashMap<String, Object>();
		// Set max-message-length of queue.
		map.put("x-max-length", 30);
		// Set time-to-live of all of messages in queue.(unit:ms)
		map.put("x-message-ttl", 6000);
		// Set dead-letter-exchange of queue.
		map.put("x-dead-letter-exchange", EXCHANGE_DL_NAME);
		// 必须配置路由,否则消息不会切换到死信交换器
		// Set dead-letter-routing-key
		map.put("x-dead-letter-routing-key", ROUTE_KEY_DL);
		return new Queue(QUEUE_NAME, true, false, false, map);
	}

	@Bean
	public Binding createBinding() {
		return BindingBuilder.bind(createQueue()).to(createDirectExchange()).with(ROUTE_KEY);
	}

	/*********** Dead letter **************/
	@Bean
	public DirectExchange createDLDirectExchange() {
		return new DirectExchange(EXCHANGE_DL_NAME, true, false);
	}

	// Create dead letter queue.
	@Bean
	public Queue createDLQueue() {
		return new Queue(QUEUE_DL_NAME, true, false, false, null);
	}

	@Bean
	public Binding createDLBinding() {
		return BindingBuilder.bind(createDLQueue()).to(createDLDirectExchange()).with(ROUTE_KEY_DL);
	}
}

死信队列的消费端与一般的没什么区别,本文就不赘述了。
死信相关概念:

  • 死信是指无法被正常消费掉的消息,而存放这类消息的队列即称为消息队列;
  • 出现死信的三点原因:
    a、消息过了设定的time-to-live时间;
    b、消息队列达到了最大的长度(x-max-length);
    c、消息的消费者拒绝了消息的接收,同时将channel.basicReject(deliveryTag,requeue)方法中的requeue参数设置为false;
2、消息消费端
2.1、消费端配置文件
server.port=8888

spring.rabbitmq.host=localhost
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
spring.rabbitmq.virtual-host=/

# 初始化的消费者数量
spring.rabbitmq.listener.simple.concurrency=5
# 最大的消费者数量
spring.rabbitmq.listener.simple.max-concurrency=10
# 每个消息费者每次处理的信息个数
spring.rabbitmq.listener.simple.prefetch=1

# 如果配置文件中配置消息手动确认,需要将以下两个都设置为manual模式(手动消息确认模式),否则,就会出现unknown delivery tag 1的错误
# 消息确认模式有三种:NONE:自动确认;AUTO:根据情况确认;MANUAL:手动确认
spring.rabbitmq.listener.direct.acknowledge-mode=manual
spring.rabbitmq.listener.simple.acknowledge-mode=manual

# 热部署
spring.devtools.restart.enabled=true
2.2、配置文件及实现消费及消息确认

实现消费;

@Configuration
@Slf4j
public class MQListenerConfig {

	/**
	 * @param message 消息内容
	 * @param channel
	 * @param headers
	 * @throws IOException
	 */
	@RabbitListener(bindings = @QueueBinding(//
			exchange = @Exchange("exchange_demo"), //
			value = @Queue("queue_demo"), //
			key = "routeKey_demo"//
	))
	@RabbitHandler
	public void receiveMessage(@Payload Message message, Channel channel, @Headers Map<String, Object> headers)
			throws IOException {
		System.err.println(message);
		long deliveryTag = (long) headers.get(AmqpHeaders.DELIVERY_TAG);

		/********** 消费端的消息确认 ************/
		// 消息确认,false:只是对当前信息的确认
		try {
			channel.basicAck(deliveryTag, false);
		} catch (IOException e) {
			log.error("MQListenerConfig exception message" + e.toString());
			//错误示范: channel.basicNack(deliveryTag, false, true);
			//正确示范: 可以将消息放入到死信队列中
			channel.basicNack(deliveryTag, false, false);	
		}
		// 重新将消息放回到队列中;true:重新将消息放回到消息队列中;false:消息放到私信队列中
		// channel.basicNack(deliveryTag, false, true);
		// 拒绝消息; true:重新将消息放回到消息队列中
		// channel.basicReject(deliveryTag, true);
	}
}

Note
接下来介绍下消费端的消息确认,当消费者(订阅)注册后,消息将被 RabbitMQ 使用basic.deliver 方法推送;该方法带有一个deliveryTagdeliveryTag是一个单调增长的正整数,唯一地标识了一个通道(channel)上的消息交付,同时消息的确认必须和接收使用同一个channel

上述的配置中使用了channel.basicNack方法将message重新放回到消息队列中,同时将方法的requeue参数设置为true,这种方式message会被放置到消息队列的头部,从而来触发消息的再次消费,但是会导致一个问题,就是重试的死循环,所以不要在catch方法这样使用basicNack方法。如果在catch中使用channel.basicNack方法,可以将requeue参数设置为false,将消息发送到死信队列中。

2.3、Message消費端限流

RabbitMQ消費端进行限流主要是使用到channel的一个方法:basicQos,有三个重载方法,如下:

/*
* @param prefetchSize  接收消息的最大Size,以字节为单位,如果为0表示没有限制
* @param prefetchCount 一次处理消息的最大数量
* @param global        false:只是对当前的消费者,true:对整个channel有效
*/
void basicQos(int prefetchSize, int prefetchCount, boolean global)
void basicQos(int prefetchCount, boolean global)
void basicQos(int prefetchCount) 

接下来可以对2.2中消息消费逻辑进行修改:

@Configuration
@Slf4j
public class MQListenerConfig {
	
	//  ... 省略
	
	@RabbitHandler
	public void receiveMessage(@Payload Message message, Channel channel, @Headers Map<String, Object> headers)
			throws IOException {
		System.err.println(message);
		long deliveryTag = (long) headers.get(AmqpHeaders.DELIVERY_TAG);
		try {
			/* 这里设置prefetchCount值为2,意味着消费端每次能最多能接收2个Message,
			   在这2个Message没有被确认之前(即:使用了basicQos就必须使用basicAck进行消息确认),消费客户端不会再接收新的Message
			*/
			channel.basicQos(0,2,false);
			
			channel.basicAck(deliveryTag, false);
		} catch (IOException e) {
			log.error("MQListenerConfig exception message" + e.toString());
			channel.basicNack(deliveryTag, false, false);	
		}
	}
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值