0. 目标
- spring AMQP
- 死信队列
- 延时队列
1. Spring AMQP
1.1 简介
Sprin有很多不同的项目,其中就有对AMQP的支持
http://spring.io/projects/spring-amqp
Spring-amqp是对AMQP协议的抽象实现,而spring-rabbit 是对协议的具体实现,也是目前的唯一实现。底层使用的就是RabbitMQ。
1.2 依赖和配置
添加AMQP的启动器:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
在application.yml
中添加RabbitMQ地址:
spring:
rabbitmq:
host: 192.168.70.136
username: guest
password: guest
virtual-host: /
1.3 配置对象
import org.springframework.amqp.core.Queue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class RabbitmqConfig {
@Bean
public Queue queue(){
return new Queue("test-queue",false,false,false);
}
}
1.4 消费者
在SpringAmqp中,对消息的消费者进行了封装和抽象,一个普通的JavaBean中的普通方法,只要通过简单的注解,就可以成为一个消费者。
@Service
public class XxxService {
// @RabbitListener:方法上的注解,声明这个方法是一个消费者方法
// 消费者方法关联的队列
@RabbitListener(queues = "test-queue")
public void test1(String message){
System.out.println("receive :"+message);
}
}
类似listen这样的方法在一个类中可以写多个,就代表多个消费者。
1.5 AmqpTemplate
Spring最擅长的事情就是封装,把他人的框架进行封装和整合。
Spring为AMQP提供了统一的消息处理模板:AmqpTemplate,非常方便的发送消息。常用的发送方法
// 指定交换机、RoutingKey和消息体
convertAndSend(String exchange,String routingKey,Object message)
// 指定消息
convertAndSend(Object message)
// 指定RoutingKey和消息,会向默认的交换机发送消息
convertAndSend(String routingKey,Object message)
1.6 生产者
@RestController
public class RabbitmqTestController {
@Autowired
private AmqpTemplate amqpTemplate;
@GetMapping("send1")
public String sendMessage(String name){
amqpTemplate.convertAndSend("test-queue","xxx:"+name);
return "send ok";
}
}
1.7 示例的解释
首先,我们配置了一个名为“test-queue”的Queue,但是并没有配置Exchange,也没有配置“test-queue”的Binding,这其实使用了RabbitMQ的默认行为,即所有Queue都以其自身的名称为routingKey绑定到了一个默认的Exchange上,该默认Exchange的名称为""
。
由于Spring Boot自动配置了AmqpAdmin,该AmqpAdmin将自动向RabbitMQ创建名为“test-queue”的Queue。
在发送消息的时候,我们直接使用了AmqpTemplate,这个AmqpTemplate是Spring Boot自动为我们配置好的,AmqpTemplate所依赖的CachingConnectionFactory也由Spring Boot自动配置。
发送消息时指定了routingKey为“test-queue”,但是没有指定Exchange,此时消息将会发送到RabbitMQ默认的Exchange,又由于名为“test-queue”的Queue向默认Exchange绑定的routingKey正是“test-queue”,因此消息将由默认Exchange转发到名为“test-queue” 的Queue。至此发送方任务结束。
在消息接收方,由于Spring Boot默认为我们配置了SimpleRabbitListenerContainerFactory,因此只需要配置@RabbitListener和@RabbitHandler接收消息即可。
Spring Boot采用了很多默认配置,通过统一的RabbitProperties
同时完成消费方和生产方的配置。
1.8 其他配置对象的创建
//队列 起名:TestDirectQueue
@Bean
public Queue queue() {
//true 是否持久
return new Queue("test-queue",true);
}
//Direct交换机 起名:directExchange
@Bean
DirectExchange directExchange() {
return new DirectExchange("directExchange");
}
//绑定
// 将队列和交换机绑定, 并设置用于匹配键:item.insert
@Bean
Binding bindingDirect() {
return BindingBuilder.bind(queue()).to(directExchange()).with("item.insert");
}
对于发送方一般来说,有交换机的配置就可以了。接收方,有交换机和队列的配置
2 死信队列
死信,顾名思义就是无法被消费的消息,字面意思可以这样理解,一般来说,producer将消息投递到broker或者直接到queue里了,consumer从queue取出消息进行消费,但某些时候由于特定的原因导致queue中的某些消息无法被消费,这样的消息如果没有后续的处理,就变成了死信,有死信,然后就有了死信队列。
-
死信队列:DLX,
dead-letter-exchange
-
利用DLX,当消息在一个队列中变成死信
(dead message)
之后,它能被重新publish到另一个Exchange,这个Exchange就是DLX
2.1 消息变成死信有以下几种情况
- 消息被拒绝(basic.reject / basic.nack),并且requeue = false
- 消息TTL过期
- 队列达到最大长度
2.2 死信的处理
- 丢弃,如果不是很重要,可以选择丢弃
- 记录死信入库,然后做后续的业务分析或处理
- 通过死信队列,由负责监听死信的应用程序进行处理
2.3 死信处理过程
- DLX也是一个正常的Exchange,和一般的Exchange没有区别,它能在任何的队列上被指定,实际上就是设置某个队列的属性。
- 当这个队列中有死信时,RabbitMQ就会自动的将这个消息重新发布到设置的Exchange上去,进而被路由到另一个队列。
- 可以监听这个队列中的消息做相应的处理。
更常用的做法是第三种,即通过死信队列,将产生的死信通过程序的配置路由到指定的死信队列,然后应用监听死信队列,对接收到的死信做后续的处理。
2.4 死信队列设置
- 首先需要设置死信队列的exchange和queue,然后进行绑定:
/**
* 定义死信队列相关信息
*/
public final static String deadQueueName = "dead_queue";
public final static String deadRoutingKey = "#";
public final static String deadExchangeName = "dead_exchange";
@Bean
public TopicExchange deadExchange() {
return new TopicExchange(deadExchangeName, true, false, null);
}
@Bean
public Queue deadQueue() {
return new Queue(deadQueueName, true, false, false);
}
@Bean
public Binding orderPublishDlqBinding() {
return BindingBuilder.bind(deadQueue() ).to(deadExchange()).with(deadRoutingKey);
}
- 然后需要有一个监听,去监听这个队列进行处理
@RabbitListener(queues = "dead_queue")
public void test1(Message message) throws UnsupportedEncodingException {
// 获取消息Id
String messageId = message.getMessageProperties().getMessageId();
String msg = new String(message.getBody(), "UTF-8");
System.out.println("私信队列处理==>邮件消费者获取生产者消息msg:" + msg + ",消息id" + messageId);
System.out.println("receive :" + message);
}
- 然后我们进行正常声明交换机、队列、绑定,只不过我们需要在队列加上一个参数即可:
arguments.put(" x-dead-letter-exchange","dlx.exchange");
,这样消息在过期、requeue、 队列在达到最大长度时,消息就可以直接路由到死信队列!
/**
* 死信队列 交换机标识符
*/
public static final String DEAD_LETTER_QUEUE_KEY = "x-dead-letter-exchange";
@Bean
public Queue emailQueue() {
// 将普通队列绑定到死信队列交换机上
Map<String, Object> args = new HashMap<>(2);
// x-dead-letter-exchange 声明了队列里的死信转发到的DLX名称,
args.put(DEAD_LETTER_QUEUE_KEY, deadExchangeName);
// x-dead-letter-routing-key 声明了这些死信在转发时携带的 routing-key 名称。
// 如果未配置x-dead-letter-routing-key则会按照原队列的key进行转发
// args.put("x-dead-letter-routing-key", "x-dead-letter-routing-key");
Queue queue = new Queue("email_queue", true, false, false, args);
return queue;
}
@GetMapping("send2")
public void send(String email) throws JsonProcessingException {
Map map = new HashMap<>();
map.put("email", email);
map.put("timestamp", 0);
String jsonString = new ObjectMapper().writeValueAsString(map);
System.out.println("jsonString:" + jsonString);
// 设置消息唯一id 保证每次重试消息id唯一
Message message = MessageBuilder.withBody(jsonString.getBytes())
.setContentType(MessageProperties.CONTENT_TYPE_JSON).setContentEncoding("utf-8")
.setMessageId(UUID.randomUUID() + "").build(); //消息id设置在请求头里面 用UUID做全局ID
amqpTemplate.convertAndSend("email_queue", message);
}
正常的消息处理
@RabbitListener(queues = RabbitmqConfig2.EMAIL_QUEUE_NAME )
public void handlerMessage3(Message message, MessageHeaders headers, Channel channel) throws Exception {
byte[] body = message.getBody();
String jsonString = new String(body,"utf-8");
System.out.println("接收到的消息:" + jsonString);
// 拒收消息后会进入死信队列
channel.basicReject(headers.get("amqp_deliveryTag",Long.class),false);
}
2.5 队列长度的指定
- 对队列中消息的条数进行限制 x-max-length
- 对队列中消息的总量进行限制 x-max-length-bytes
对消息总条数进行限制(总条数包括未被消费的消息+被消费但未被确认的消息):
3. 延时消息
可以通过设置消息的expiration字段或者x-message-ttl属性来设置时间(单位是毫秒),两者是一样的效果。只是expiration字段是字符串参数,所以要写个int类型的字符串:
Message message = MessageBuilder.withBody(jsonString.getBytes())
.setContentType(MessageProperties.CONTENT_TYPE_JSON).setContentEncoding("utf-8")
// 设置延时时间
.setExpiration(3+"")
.setMessageId(UUID.randomUUID() + "").build(); //消息id设置在请求头里面 用UUID做全局ID