一、业务场景:
在商城项目中,必定会遇到下单支付的功能:用户购买商品--提交订单(预订单)--支付页面(30分钟内支付)
对于这种场景,往往需要生成预订单,等待某个时间段之后,如果用户未支付,会失效/删除 这个预订单。
二、思路
预订单即任务
- 定时处理任务
- 延时处理任务
三、参考方法
1. 定时任务
- 用户下单后,创建一个定时任务,到期后去检查订单是否支付。
- 定时轮询一下数据库/缓存,查看订单状态。通过
当前时间-下单时间判断是否过期等。
2. 延时任务
- java 自带延迟队列 DelayedQueue。jvm自带,缺点很明显:停机/重启 啪!数据没了。
- redis 监听过期key。适用于业务比较单一的场景。且redis集群模式需要做兼容处理。
- rabbitMQ 死信队列-原理:
四、编码
1. 延迟队列方式
import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;
/**
* @author Agao
* @date 2024/1/24 17:01
*/
public class DiyDelayQueue<T> implements Delayed {
/**
* 延迟时间
*/
long delayTime;
/**
* 过期时间
*/
long expire;
/**
* 任务数据
*/
T data;
/**
* 有参构造,构造延迟队列中的数据源
*
* @param delayTime 延迟时间 eg:30
* @param delayTimeUnit 单位 eg:分
* @param t 数据 eg: 数据对象
*/
public DiyDelayQueue(long delayTime, TimeUnit delayTimeUnit, T t) {
// 转成时间毫秒数,结合时间戳使用
this.delayTime = TimeUnit.MILLISECONDS.convert(delayTime, delayTimeUnit);
// 过期时间
this.expire = System.currentTimeMillis() + delayTime;
this.data = t;
}
@Override
public long getDelay(TimeUnit unit) {
// 注意convert这个方法,第一个参数是一个long类型的数值,第二个参数的意思是告诉convert第一个long类型的值的单位是毫秒
return unit.convert(this.expire - System.currentTimeMillis(), TimeUnit.MILLISECONDS);
}
@Override
public int compareTo(Delayed o) {
long f = this.getDelay(TimeUnit.MILLISECONDS) - o.getDelay(TimeUnit.MILLISECONDS);
return (int) f;
}
}
2. 利用 Redis 定期失效key的特性--实现key过期的监听器
// 订单key 缓存,30分钟后过期
String key = orderId;
redisUtil.set(key,System.currentTimeMillis(), 30, TimeUnit.MINUTES);
/**
* redis key 过期监听器
*
* @author Agao
* @date 2024/1/18 17:22
*/
@Slf4j
@Component
public class RedisKeyExpireListener extends KeyExpirationEventMessageListener {
public RedisKeyExpireListener(RedisMessageListenerContainer listenerContainer) {
super(listenerContainer);
}
@Override
public void onMessage(Message message, byte[] pattern) {
String expiredKey = message.toString();
log.info("redis key: {} expired", expiredKey);
// 订单 超时未支付
if (expiredKey.startsWith("按照你的订单规则")) {
// 预订单过期,逻辑处理
}
}
}
3. rabbitMQ 死信队列
MQ配置文件
spring:
rabbitmq:
host: localhost
port: 5672
username: admin
password: admin
template:
retry:
enabled: true
initial-interval: 2s
virtual-host: /
listener:
simple:
# 每个队列启动的消费者数量
concurrency: 1
# 每个队列最大的消费者数量
max-concurrency: 1
# 手动签收ACK
acknowledge-mode: manual
# 每次从RabbitMQ获取的消息数量
prefetch: 1
- 配置rabbitMQ
import org.springframework.amqp.core.*;
import org.springframework.amqp.rabbit.connection.CachingConnectionFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
import java.util.Map;
/**
* MQ 配置类
*
* @author Agao
* @date 2024/1/24 17:31
*/
@Configuration
public class RabbitMQConfig {
/** 队列名称 */
public static final String ORDER_QUEUE = "order_queue";
/** 交换机名称 */
public static final String ORDER_EXCHANGE = "order_exchange";
/** 订单路由key */
public static final String ROUTING_KEY_ORDER = "routing_key_order";
/** 死信消息队列名称 */
public static final String DEAL_QUEUE_ORDER = "deal_queue_order";
/** 死信交换机名称 */
public static final String DEAL_EXCHANGE_ORDER = "deal_exchange_order";
/** 死信 routingKey */
public static final String DEAD_ROUTING_KEY_ORDER = "dead_routing_key_order";
@Bean
public Queue orderQueue() {
// 将普通队列绑定到死信队列交换机上
return QueueBuilder.durable(ORDER_QUEUE)
.deadLetterExchange(DEAL_EXCHANGE_ORDER)
.deadLetterRoutingKey(DEAD_ROUTING_KEY_ORDER)
.build();
}
// 声明一个direct类型的交换机
@Bean
public DirectExchange orderExchange() {
return new DirectExchange(RabbitMQConfig.ORDER_EXCHANGE);
}
// 绑定Queue队列到交换机,并且指定routingKey
@Bean
public Binding bindingDirectExchange() {
return BindingBuilder.bind(orderQueue()).to(orderExchange()).with(ROUTING_KEY_ORDER);
}
// 创建配置死信队列
@Bean
public Queue deadQueueOrder() {
return QueueBuilder.durable(DEAL_QUEUE_ORDER).build();
}
// 创建死信交换机
@Bean
public DirectExchange deadExchangeOrder() {
return new DirectExchange(DEAL_EXCHANGE_ORDER);
}
// 死信队列与死信交换机绑定
@Bean
public Binding bindingDeadExchange() {
return BindingBuilder.bind(deadQueueOrder())
.to(deadExchangeOrder())
.with(DEAD_ROUTING_KEY_ORDER);
}
}
- 生产者
@Slf4j
@RestController
public class OrderController {
@Autowired private AmqpTemplate amqpTemplate;
@Autowired private ObjectMapper objectMapper;
public void preOrder(PreOrderRo ro) throws JsonProcessingException {
log.info("生成预订单----orderId: {}", ro.getId());
Order order = new Order();
order.setId(UUID.randomUUID().toString());
order.setPrice(ro.getPrice());
order.setStatus(OrderStatus.WAIT_PAY.getCode());
order.setCreateDate(System.currentTimeMillis());
// 投递消息
amqpTemplate.convertAndSend(
RabbitMQConfig.ORDER_EXCHANGE,
RabbitMQConfig.ROUTING_KEY_ORDER,
objectMapper.writeValueAsString(order),
message -> {
// 也可以直接设置queue 的过期时间
message.getMessageProperties().setExpiration(1000 * 10 + "");
return message;
});
log.info("订单生产者结束----------");
}
}
- 消费者
import com.agao.config.RabbitMQConfig;
import com.rabbitmq.client.Channel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.messaging.handler.annotation.Headers;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.util.Map;
/**
* 订单过期监听器
*
* @author Agao
* @date 2024/1/24 17:41
*/
@Slf4j
@Component
public class OrderExpireListener {
@Autowired private ObjectMapper objectMapper;
// 设置订单失效的队列
@RabbitListener(queues = RabbitMQConfig.DEAL_QUEUE_ORDER)
public void process(
@Payload String orderMsg,
Message message,
@Headers Map<String, Object> headers,
Channel channel)
throws IOException {
Order order = objectMapper.readValue(orderMsg, Order.class);
log.info("order pay expire :{}", order);
// 判断订单是否支付,做未支付的业务处理
// 手动ack
Long deliveryTag = (Long) headers.get(AmqpHeaders.DELIVERY_TAG);
// 手动签收
channel.basicAck(deliveryTag, false);
}
}