一、业务场景
当下完订单,一般超过15分钟或者30分钟就需要对未支付的订单进行关闭。
二、解决方案
1.定时任务
通过定时任务隔一段时间扫描一次的数据,超过时间的订单,修改为取消状态
public class OrderCloseTask {
@Scheduled(cron = "0 0/15 * * * ?")
public void closeOrder() {
// .. 关闭订单
}
}
问题:
- 时效性差
定时任务每15分钟扫一次,如果14:59创建了的单子,那关闭订单的时候就滞后了 - 性能差
每隔一段时间就要扫描一次表数据,扫描表操作数据的时候,可能会加锁,性能比较低
2.redis pub/sub 发布订阅
下单,在redis创建一个带失效时间的key。通过key失效,监听key失效的事件,去取消订单
package com.xiaokk.house.web.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.listener.KeyExpirationEventMessageListener;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import java.nio.charset.StandardCharsets;
/**
* 功能:
*
* @author kangping
* @date 2022-01-29 1:20 下午
*/
@Slf4j
package com.xiaokk.house.web.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.listener.KeyExpirationEventMessageListener;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import java.nio.charset.StandardCharsets;
/**
* 功能:
*
* @author kangping
* @date 2022-01-29 1:20 下午
*/
@Slf4j
public class KeyExpiredListener extends KeyExpirationEventMessageListener {
/**
* Creates new {@link MessageListener} for {@code __keyevent@*__:expired} messages.
*
* @param listenerContainer must not be {@literal null}.
*/
public KeyExpiredListener(RedisMessageListenerContainer listenerContainer) {
super(listenerContainer);
}
@Override
public void onMessage(Message message, byte[] pattern) {
// 失效的key
String key = new String(message.getBody(), StandardCharsets.UTF_8);
log.info("key = {}", key);
// 消息来自于那个渠道,redis默认有16个库,就有16个渠道
String channel = new String(message.getChannel(), StandardCharsets.UTF_8);
log.info("channel = {}", channel);
// 监听的渠道 "__keyevent@*__:expired 默认兼容所有的数据库
String patt = new String(pattern, StandardCharsets.UTF_8);
log.info("patt = {}",patt);
// 取消订单
}
}
创建消息监听的一个容器
@Bean
public RedisMessageListenerContainer redisMessageListenerContainer(RedisConnectionFactory redisConnectionFactory) {
RedisMessageListenerContainer redisMessageListenerContainer = new RedisMessageListenerContainer();
redisMessageListenerContainer.setConnectionFactory(redisConnectionFactory);
return redisMessageListenerContainer;
}
@Bean
public KeyExpiredListener keyExpiredListener(RedisMessageListenerContainer redisMessageListenerContainer) {
return new KeyExpiredListener(redisMessageListenerContainer);
}
问题
- 发布到所有的订阅者
一般订单服务会有多个,假如订单服务有3个,那3个都会去取消订单。如果想到加入分布式锁,如果抢到锁的服务挂了。取消订单就是失败了。就没有了可靠性 - 性能问题
keyevent@*:expired 默认是监听的所有渠道,代表不是订单取消相关的key也会进入到这个监听方法。 - 时效性
redis 的key 是维护在一个字典表当中,redis是每1秒去检查几次这些key,每次检查中是抽取一定的比例的key,看这些key是否已经失效。如果key比较多,可能就有个别的key总是没有被抽取到,导致key没有及时失效。在抽取的key当中,如果超过一定量的比例都失效,redis会减少去检查的时间,导致检查得更加频繁。而woker本身是单线程,所有有性能损耗。
3.延时消息
创建订单的时候发送一个延时消息。消费的时候判断订单是否还是待支付状态,如果是,则关闭订单
优点
- 正常情况下,只有一个订单服务会去消费消息
- ask机制可以保证,至少消费一次。保证了可靠性
缺点
发送消息可能是有网络延时的,或者mq队列中消息很多,时间到了还没有被消费到。还是会有实时性问题。
解决方案:
通过业务方式去处理,在支付的时候去判断这个订单是否是已经过期,如果已经过期,在取消订单