前言
有很多场景需要延迟一段时间完成,例如订单超过一定时间未支付等。可以用定时任务实现,也可以用消息队列MQ来实现。近些时日,学了一些,记录一下。不再赘述rabbitMQ以及插件的安装, 3.5.7版本以后支持延迟插件。
一、准备
springboot结合rabbitMQ只需要添加jar包,配置一下就可以了。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
application.properties配置文件里配置一下服务地址。
spring.rabbitmq.host=localhost
spring.rabbitmq.port=5672
spring.rabbitmq.username=admin
spring.rabbitmq.password=admin
spring.rabbitmq.virtual-host=/
spring.rabbitmq.connection-timeout=15000
#采用消息确认模式,消息发出去后,异步等待响应
spring.rabbitmq.publisher-confirms=true
spring.rabbitmq.publisher-returns=true
spring.rabbitmq.template.mandatory=true
#消费端配置
#消费者监听并发数
spring.rabbitmq.listener.simple.concurrency=10
#最大并发数
spring.rabbitmq.listener.simple.max-concurrency=20
#签收模式 推荐使用manual手工签收
spring.rabbitmq.listener.simple.acknowledge-mode=manual
#限流
spring.rabbitmq.listener.simple.prefetch=5
二、声明队列和交换机
声明处理消息的队列,通过routingkey绑定交换机,注意交换机为CustomExchange。
@Configuration
public class DelayMessageConfig {
public static final String DELAY_EXCHANGE_NAME = "lwq_delay_exchange";
public static final String DELAY_QUEUE_NAME = "lwq_delay_queue";
public static final String ROUTING_KRY = "lwq_delay_queue";
/**
* 声明一个延迟队列
* @return
*/
@Bean
Queue delayQueue(){
return QueueBuilder.durable(DELAY_QUEUE_NAME).build();
}
/**
* 声明一个交换机
* @return
*/
@Bean
CustomExchange delayExchange(){
Map<String, Object> args = new HashMap<>();
args.put("x-delayed-type", "direct");
return new CustomExchange(DELAY_EXCHANGE_NAME, "x-delayed-message", true,false, args);
}
/**
* 绑定
* @param delayQueue
* @param delayExchange
* @return
*/
@Bean
Binding queueBinding(Queue delayQueue, CustomExchange delayExchange){
return BindingBuilder.bind(delayQueue).to(delayExchange).with(ROUTING_KRY).noargs();
}
}
三、生产者
生产者中直接注入RabbitTemplate就可以使用了,调用convertAndSend发送消息。为了保证消息投递的可靠性,可以为RabbitTemplate设置监听函数,然后将消息发送状态写入数据库等。
rabbitTemplate.setConfirmCallback(confirmCallback);
监听函数要实现RabbitTemplate下的ConfirmCallBack接口,重写confirm方法,根据ack判断是否投递成功,投递成功则修改消息的状态为成功。另外可以设置一个定时任务,定时从数据库拿中间状态(不知道失败还是成功)的消息重新发送,尝试三次后投递还是失败可以设置消息状态为失败,再进行人工处理。这里只是代码示例如何设置监听函数,消息的可靠投递详细可参考RabbitMQ消息中间件极速入门与实战。
@Component
public class DelayMessageSender {
private static final Logger log = LoggerFactory.getLogger(DelayMessageSender.class);
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* 生产者回调函数:confirm确认消息投递成功
*/
final ConfirmCallback confirmCallback = new RabbitTemplate.ConfirmCallback() {
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
String messageId = correlationData.getId();
if (ack) {
log.info("消息投递成功,{}",messageId);
//进行消息记录的数据库更新
}else{
log.info("消息投递失败");
}
}
};
/**
* 通过延迟消息插件发动延迟消息
* @param msg
* @param expiration
*/
public void sendDelayMessageByPlugins(Object msg,Long expiration){
//绑定异步监听回调函数
rabbitTemplate.setConfirmCallback(confirmCallback);
rabbitTemplate.convertAndSend(DelayMessageConfig.DELAY_EXCHANGE_NAME,DelayMessageConfig.ROUTING_KRY, msg,(message)->{
message.getMessageProperties().setHeader("x-delay", expiration);//设置延迟时间
return message;
},new CorrelationData("123"));
}
}
四、消费者
消费者端使用@RabbitListener注解监听第二步声明的队列即可,消费者建议使用手工签收模式
@Component
public class DelayMessageReveiver {
@RabbitListener(queues = "lwq_delay_queue")
@RabbitHandler
public void receive(@Payload String msg,@Headers Map<String, Object> headers,Channel channel) throws Exception {
System.out.println("接收到的消息:"+msg +"||"+LocalDateTime.now());
//业务逻辑
//ACK 手工签收,通知rabbitMQ,消费端消费成功
Long deliveryTag = (Long)headers.get(AmqpHeaders.DELIVERY_TAG);
channel.basicAck(deliveryTag,false);
}
}
结语
不用延迟插件实现延迟消息还是挺麻烦的,有了插件,简直是程序员的一大福利啊。
源码参考我的码云:https://gitee.com/waynelee/rabbitMQ