在使用rabbitmq实现延迟消费时,需要先明白什么是死信队列,什么是延迟队列。
rabbitmq并没有直接支持延迟队列,延迟队列是通过死信队列来实现的。
死信队列
DLX(Dead Letter Exchange),死信交换器。当队列中的消息被拒绝、或者过期会变成死信,死信可以被重新发布到另一个交换器,这个交换器就是DLX,与DLX绑定的队列称为死信队列。
造成死信的原因:
信息被拒绝
信息超时
超过了队列的最大长度
延迟队列
延迟队列存储的是延迟消息,延迟消息指的是,当消息被发发布出去之后,并不立即投递给消费者,而是在指定时间之后投递。如:在订单系统中,订单有30秒的付款时间,在订单超时之后在投递给消费者处理超时订单。
rabbitMq没有直接支持延迟队列,可以通过死信队列实现。在死信队列中,可以为普通交换器绑定多个消息队列,假设绑定过期时间为5分钟,10分钟和30分钟,3个消息队列,然后为每个消息队列设置DLX,为每个DLX关联一个死信队列。当消息过期之后,被转存到对应的死信队列中,然后投递给指定的消费者消费。
整体项目结构图
父pom文件
<!-- 定义公共资源版本 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.5.RELEASE</version>
<relativePath />
</parent>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.5.RELEASE</version>
</dependency>
</dependencies>
</dependencyManagement>
死信队列生产者
pom文件
<dependencies>
<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.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
application.yml文件
spring:
rabbitmq:
host: 127.0.0.1
port: 5672
username: admin
password: admin
#发布者确认
publisher-confirms: true
server:
port: 9041
rabbitmq:
#死信队列(最终消费者会消费此队列)
dlx:
exchange: mayikt_order_dlx_exchange
queue: mayikt_order_dlx_queue
routingKey: dlx
driver:
exchange: exchange
routingKey: routingkey
queue: queue
rabbitmq配置
@Configuration
public class RabbitmqConfiguration {
/**
* exchange交换器名字
*/
@Value("${rabbitmq.driver.exchange}")
private String driverUpExchangeName;
/**
* 路由键名字
*/
@Value("${rabbitmq.driver.routingkey}")
private String driverUpRoutingKey;
/**
* queue
*/
@Value("${rabbitmq.driver.queue}")
private String driverUpQueue;
/**
* 死信交换机
*/
@Value("${rabbitmq.dlx.exchange}")
private String dlxExchange;
/**
* 死信队列
*/
@Value("${rabbitmq.dlx.queue}")
private String dlxQueue;
/**
* 死信路由
*/
@Value("${rabbitmq.dlx.routingKey}")
private String dlxRoutingKey;
@Autowired
private RabbitTemplate rabbitTemplate;
@PostConstruct
public void initRabbitTemplate() {
// 设置生产者消息确认
rabbitTemplate.setConfirmCallback(new RabbitConfirmCallback());
}
//死信交换机
@Bean
public DirectExchange dlxExchange() {
return new DirectExchange(dlxExchange);
}
//声明死信队列 (真正的队列)
@Bean
public Queue dlxQueue() {
return new Queue(dlxQueue);
}
/**
* 声明订单业务交换机(基本交换机)
*/
@Bean
public DirectExchange orderExchange() {
return new DirectExchange(driverUpExchangeName);
}
/**
* 声明订单队列(用于基本交换机和基本路由到死信队列的绑定)
*/
@Bean
public Queue orderQueue() {
Map<String, Object> arguments = new HashMap<>(2);
// 绑定我们的死信交换机
arguments.put("x-dead-letter-exchange", dlxExchange);
// 绑定我们的路由key
arguments.put("x-dead-letter-routing-key", dlxRoutingKey);
return new Queue(driverUpQueue, true, false, false, arguments);
}
/**
* (创建基本交换机+基本路由 -> 死信队列 的绑定)
*/
@Bean
public Binding orderBinding() {
return BindingBuilder.bind(orderQueue()).to(orderExchange()).with(driverUpRoutingKey);
}
/**
* (死信交换机+死信路由->真正队列 的绑定)
*/
@Bean
public Binding binding() {
return BindingBuilder.bind(dlxQueue()).to(dlxExchange()).with(dlxRoutingKey);
}
}
生产者消息确认
@Slf4j
public class RabbitConfirmCallback implements RabbitTemplate.ConfirmCallback {
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
log.info("(start)生产者消息确认=========================");
log.info("correlationData:[{}]", correlationData);
log.info("ack:[{}]", ack);
log.info("cause:[{}]", cause);
if (!ack) {
log.info("消息可能未到达rabbitmq服务器");
}
log.info("(end)生产者消息确认=========================");
}
}
controller
@RestController
@RequestMapping("/rabbitmq")
public class SendController {
@Autowired
private DriverRabbitmqServce driverRabbitmqServce;
@PostMapping("/send")
public void send() {
driverRabbitmqServce.sendDriverMessage(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()) + " 发送了死信消息");
}
}
service
@Slf4j
@Service
public class DriverRabbitmqServce {
/**
* exchange交换器名字
*/
@Value("${rabbitmq.driver.exchange}")
private String driverUpExchangeName;
/**
* 路由键名字
*/
@Value("${rabbitmq.driver.routingkey}")
private String driverUpRoutingKey;
@Autowired
private AmqpTemplate amqpTemplate;
//处理待发送消息()
private MessagePostProcessor messagePostProcessor() {
return new MessagePostProcessor() {
@Override
public Message postProcessMessage(Message message) throws AmqpException {
//设置有效期 毫秒单位 后过期
message.getMessageProperties().setExpiration("10000");
return message;
}
};
}
public void sendDriverMessage(String driverNumber) {
//向消息队列发送消息
//参数一 : 交换器 参数三 : 消息
this.amqpTemplate.convertAndSend(driverUpExchangeName, driverUpRoutingKey, driverNumber, messagePostProcessor());
log.info("向消息队列发送消息 {}", driverNumber);
}
}
项目结构如图
死信队列消费者
pom文件
<dependencies>
<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.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
application.yml
spring:
rabbitmq:
host: 127.0.0.1
port: 5672
username: admin
password: admin
listener:
simple:
#设置消费端手动 ack none不确认 auto自动确认 manual手动确认
acknowledge-mode: manual
retry:
#开启消费者重试机制(为false时关闭消费者重试(开启消息重新投递),这时消费端代码异常会一直重复收到消息)
enabled: true
#重试次数5(重新投递次数)
max-attempts: 5
#重试时间间隔
initial-interval: 5000
#重试次数超过上面的设置之后是否丢弃(消费者listener抛出异常,是否重回队列,默认true:重回队列, false为不重回队列(结合死信交换机)
# default-requeue-rejected: true
#消费之最大数量
max-concurrency: 1
server:
port: 9040
rabbitmq:
dlx:
name: mayikt_order_dlx_name
exchange: mayikt_order_dlx_exchange
queue: mayikt_order_dlx_queue
routingKey: dlx
消费者
@Slf4j
@Component
@RabbitListener(
//绑定队列
bindings = @QueueBinding(
//需要指定queue的名字
value = @Queue(
//配置队列名称
value = "${rabbitmq.dlx.name}",
//是否是一个可删除的临时队列 --> 消息持久化
autoDelete = "false"
),
//配置交换器
exchange = @Exchange(
//指定交换器名称
value = "${rabbitmq.dlx.exchange}",
//指定具体的交换器类型,有常量类指定
type = ExchangeTypes.DIRECT
),
//路由键
key = "${rabbitmq.dlx.routingkey}"
)
)
public class DriverListen {
/**
* 接受消息的方法,采用消息队列监听机制
*/
@RabbitHandler
public void pay(String msg, Message message, Channel channel) throws Exception {
log.info("consumers 接受到了 rabbitmq 消息 {}", msg);
//消费者手动ack机制
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
/**
* 下面代码测试异常重试机制
*/
// try {
// int i = 1 / 0;
// } catch (Exception e) {
// //如果不抛出异常,catch之后无法重试
// throw new RuntimeException("我异常了");
// }
}
}
项目结构图
只需要修改配置文件即可使用,相关位置都标明了注解,易于上手
项目测试,启动消费者和生产者,调用生产者的controller接口即可。查看消费者的消费时间。
关注公众号
每周会更新干货知识