引言
最近工作中开始接触支付、订单等相关业务内容,想要良好的解决订单超时未支付自动关闭的问题。传统解决方法有两种:
- 被动触发。只有当用户或商户查询订单信息时,再判断该订单是否超时,如果超时再进行超时逻辑的处理。这种做法实现简单,但是这种做法会导致用户体验极差,打开订单时需要极多的处理判断,甚至会对库存、订单量的统计带来误差。
- 写同步定时任务,定时扫描数据库表中的数据。这种处理方式只是适用比较小而简单的项目,当业务规模扩展时,依旧会带来很多问题,比如:效率极低;订单关闭不及时,订单自动关闭的及时与否取决于设置的扫描时间窗口,如果你设置每分钟定时轮询一次,那么订单取消时间的最大误差就有一分钟。当然也可能更大,比如一分钟之内有大量数据,但是一分钟没处理完,那么下一分钟的就会顺延;增加数据库的压力等。
在查阅资料之后,我们的解决方案寄望于延时队列的处理方式上。
延时队列的这种实现方式,包含两个重要的数据结构:环形队列,例如可以创建一个包含 2400 个 slot 的环形队列(本质是个数组);任务集合,环上每一个 slot 是一个 Set。本质上,就是一个时间轮算法的一个实现。
时间轮算法可以类比于时钟,如上图箭头(指针)按某一个方向按固定频率轮动,每一次跳动称为一个 tick。这样可以看出定时轮由个3个重要的属性参数,ticksPerWheel(一轮的 tick 数),tickDuration(一个 tick 的持续时间)以及 timeUnit(时间单位),例如当 ticksPerWheel=60,tickDuration=1,timeUnit =秒,这就和现实中的始终的秒针走动完全类似了。
如果当前指针指在 1 上面,我有一个任务需要 4 秒以后执行,那么这个执行的线程回调或者消息将会被放在 5 上。那如果需要在20秒之后执行怎么办,由于这个环形结构槽数只到 7,如果要 20 秒,指针需要多转2圈。位置是在2圈之后的 5 上面(20 % 7 + 1)。
目前有很多消息队列都支持,比如 RocketMQ,RabbitMQ 等,本文以RabbitMQ为例,介绍如何使用RabbitMQ进行延时队列的创建。
1. Docker 安装Rabbit MQ
docker pull rabbitmq:management
docker run -dit --name rabbitmq -e RABBITMQ_DEFAULT_USER=admin -e RABBITMQ_DEFAULT_PASS=password -p 15672:15672 -p 5672:5672 rabbitmq:management
2. Rabbit MQ安装延迟队列插件
下载与安装的Rabbitmq匹配的插件
rabbitmq-delayed-message-exchange
上传到服务器的/root文件夹下,然后进行如下操作
docker cp /root/rabbitmq_delayed_message_exchange-3.8.0.ez rabbitmq:/plugins
docker exec -it rabbitmq /bin/bash
rabbitmq-plugins enable rabbitmq_delayed_message_exchange
rabbitmq-plugins list
看到如下图所示即为安装成功
3. pom引入Rabbit MQ
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
4. 创建RabbitConfig配置类
@Configuration
@RequiredArgsConstructor
public class RabbitConfig {
private final CachingConnectionFactory connectionFactory;
private final SimpleRabbitListenerContainerFactoryConfigurer factoryConfigurer;
/**
* 多个消费者
*
* @return SimpleRabbitListenerContainerFactory
*/
@Bean(name = "multiListenerContainer")
public SimpleRabbitListenerContainerFactory multiListenerContainer() {
SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
factoryConfigurer.configure(factory, connectionFactory);
factory.setMessageConverter(new Jackson2JsonMessageConverter());
factory.setAcknowledgeMode(AcknowledgeMode.NONE);
factory.setConcurrentConsumers(10);
factory.setMaxConcurrentConsumers(15);
factory.setPrefetchCount(10);
return factory;
}
@Bean
public Queue reserveDelayQueue() {
return new Queue("order.delay.queue.name", true);
}
@Bean
public CustomExchange reserveDelayExchange() {
Map<String, Object> pros = new HashMap<>(2);
pros.put("x-delayed-type", "topic");
return new CustomExchange("order.delay.exchange.name", "x-delayed-message", true, false, pros);
}
@Bean
public Binding reserveDelayBindingNotify(@Qualifier("reserveDelayQueue") Queue queue,
@Qualifier("reserveDelayExchange") CustomExchange customExchange) {
return BindingBuilder.bind(queue).to(customExchange).with("order.delay.routing.key.name").noargs();
}
}
3. 创建生产者
@Slf4j
@Component
@RequiredArgsConstructor
public class CancelOrderProducer {
private final RabbitTemplate rabbitTemplate;
public void sendMsg(Long orderId, Integer delayTime){
rabbitTemplate.setMessageConverter(new Jackson2JsonMessageConverter());
// 设置基本交换机
rabbitTemplate.setExchange("order.delay.exchange.name");
// 设置基本路由
rabbitTemplate.setRoutingKey("order.delay.routing.key.name");
rabbitTemplate.convertAndSend(orderId, new MessagePostProcessor() {
@Override
public Message postProcessMessage(Message message) throws AmqpException {
MessageProperties messageProperties = message.getMessageProperties();
messageProperties.setDeliveryMode(MessageDeliveryMode.PERSISTENT);
message.getMessageProperties().setDelay(delayTime);
return message;
}
});
log.info("用户下单支付超时-发送用户下单记录id的消息到延迟队列:orderId={}", orderId);
}
}
4. 业务中引用
@Slf4j
@Service
@RequiredArgsConstructor
public class ReserveServiceImpl extends ServiceImpl<ReserveMapper, HaReserve> implements ReserveService {
// 引入生产者
private final CancelOrderProducer cancelOrderProducer;
@Value("${reserve.release-time}")
private int autoCloseTime;
@Override
public JSONObject doReserve(ReserveDTO reserveDTO) {
... ...
// 省略业务代码
cancelOrderProducer.sendMsg(reserve.getId(), autoCloseTime);
return null;
}
}
5. 创建消费者接收超时消息
具体的业务处理写在此处,一定注意要消费后要手动提交ack!!!
@Slf4j
@Component
@RequiredArgsConstructor
public class CancelOrderConsumer {
private final RemoteBasicService remoteBasicService;
@RabbitHandler
@RabbitListener(queues = "order.delay.queue.name", containerFactory = "multiListenerContainer")
public void handle(@Payload String orderId, Channel channel, Message message) {
try {
//业务处理
log.info("用户下单支付超时取消订单-取消订单id:orderId={}",orderId);
handleService(orderId, channel, message);
} catch (Exception e) {
log.error("{}", e.getMessage(), e);
}
}
public void handleService(String orderId, Channel channel, Message message) throws IOException, InterruptedException {
// channel.basicAck(long,boolean); 确认收到消息,消息将被队列移除,false只确认当前consumer一个消息收到,true确认所有consumer获得的消息。
// channel.basicNack(long,boolean,boolean); 确认否定消息,第一个boolean表示一个consumer还是所有,第二个boolean表示requeue是否重新回到队列,true重新入队。
// channel.basicReject(long,boolean); 拒绝消息,requeue=false 表示不再重新入队,如果配置了死信队列则进入死信队列。
// 具体处理的业务代码省略
... ...
}
参考文章