背景
需求需要实现 订单15分钟超时未支付自动关闭
调研实现方案
- 基于java DelayQueue
缺点: 单机、不能持久化、宕机任务丢失等等;
优点:不依赖任何三方,仅java原生api即可 - 定时任务全表扫描
缺点:需要全表扫描,任务设置轮询时间就是最大延迟时间,对数据库有一定压力,仅适合数据量少的业务场景
优点: 实现简单,仅需要任务调度即可 - redis过期消息通知
缺点:
- 开启键通知会对redis有额外的开销
- 键通知暂时redis并不保证消息必达,redis客户端断开连接所有key丢失
- 消费速度不可自控,如果一瞬间QPS非常高,接收到的通知会非常密集,消费不过来,
如果用线程池消费,大部分的待消费任务会放入到阻塞队列 一旦服务宕机,阻塞队列消息全部丢失
- mq提供的延时队列
优点:消息0丢失,可抗高并发
缺点:需要额外引入mq中间件,提高系统复杂性和mq高可用维护性 - 借鉴redis的惰性删除策略:订单过期时不删除,在查询订单时对订单过期时间作校验,如果过期则删除
优点:减少对过期订单的检测,提高cpu利用率
缺点:如果一直不访问订单,则库存一直无法回滚
延时队列简单实现
- 依赖
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.12</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.3.2</version>
</dependency>
order
{
//订单编号
private String orderNO;
//过期时间
private Date time;
//订单状态
private String orderStatus;
// 设置延时时间
public long getDelay(TimeUnit unit) {
long l = unit.convert(time.getTime() - System.currentTimeMillis(), TimeUnit.MILLISECONDS);
return l;
}
public int compareTo(Delayed o) {
//根据取消时间来比较,如果取消时间小的,就会优先被队列提取出来
return time.compareTo(((Order) o).getTime());
}
}
DelayedTest
public class DelayedTest {
// 自动取消线程开关
static boolean flag = true;
//存放过期订单
static DelayQueue<Order> queue = new DelayQueue();
public static void main(String[] args) {
DelayedTest delayedTest = new DelayedTest();
//新建一个线程,用来模拟定时取消订单job
new Thread(() -> {
System.out.println("开启自动取消订单job,当前时间:"+ DateUtil.format(LocalDateTime.now(), DatePattern.NORM_DATETIME_PATTERN));
while (flag) {
try {
Order order = delayedTest.queue.take();
order.setOrderStatus("超时取消");
System.out.println("订单:" + order.getOrderNO() + "超时取消"+ DateUtil.format(LocalDateTime.now(), DatePattern.NORM_DATETIME_PATTERN));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
new Thread(() -> {
//定义最早的订单的创建时间
long now = System.currentTimeMillis();
System.out.println("下单开始时间" + DateUtil.format(new Date(now), DatePattern.NORM_DATETIME_PATTERN));
//下面模拟6个订单,每个订单的创建时间依次延后3秒
queue.add(new Order("001", new Date(now + 3000), "未支付"));
queue.add(new Order("002", new Date(now + 6000), "未支付"));
queue.add(new Order("003", new Date(now + 9000), "未支付"));
queue.add(new Order("004", new Date(now + 12000), "未支付"));
}).start();
}
}
redis简单实现
框架: spirngboot
- 依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
- 配置文件添加redis配置
spring.redis.port=6379
spring.redis.host=49.233.150.105
- redis开启过期键通知
找到redis配置文件redis.conf,打开配置文件找到notify-keyspace-events
配置查看是否开启,默认是没有开启的,添加配置notify-keyspace-events Ex
重启redis
相关参数说明:
K:keyspace 事件,事件以 keyspace@ 为前缀进行发布
E:keyevent 事件,事件以 keyevent@ 为前缀进行发布
g:一般性的,非特定类型的命令,比如del,expire,rename等
$:字符串特定命令
l:列表特定命令
s:集合特定命令
h:哈希特定命令
z:有序集合特定命令
x:过期事件,当某个键过期并删除时会产生该事件
e:驱逐事件,当某个键因 maxmemore 策略而被删除时,产生该事件
A:g$lshzxe的别名,因此”AKE”意味着所有事件
- 配置 RedisListenerConfig 实现监听 Redis key 过期时间
RedisListenerConfig
@Configuration
public class RedisListenerConfig {
@Bean
RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
return container;
}
}
- 定义监听器 RedisKeyExpirationListener,实现KeyExpirationEventMessageListener 接口
RedisKeyExpirationListener
@Component
public class RedisKeyExpirationListener extends KeyExpirationEventMessageListener {
@Autowired
RedisTemplate redisTemplate;
@Autowired
OrderDao orderDao;
public RedisKeyExpirationListener(RedisMessageListenerContainer listenerContainer) {
super(listenerContainer);
}
/**
* 针对 redis 数据失效事件,进行数据处理
*
* @param message
* @param pattern
*/
@Override
public void onMessage(Message message, byte[] pattern) {
RedisSerializer<String> serializer = redisTemplate.getValueSerializer();
// 获取到失效的 key
String orderNo = message.toString();
if (StrUtil.startWith("", "order")) {
}
OrderSummaryDO orderSummaryDO = new OrderSummaryDO();
orderSummaryDO.setOrderNo(orderNo);
orderSummaryDO.setOrderStatus(OrderStatusEnum.CLOSE.getCode());
orderDao.updateOrderStatus(orderSummaryDO);
}
}