大家好,我是一安,之前有介绍使用RabbitMQ延时队列实现,一种是基于死信队列,但使用死信会存在两个弊端,所以又提到一种基于rabbitmq_delayed_message_exchange
插件的方式,这两种是比较常见的和使用的,面试能答出来这两种,应该没什么问题,但我们不能局限这两种,下面就介绍一下几种不同的实现方式
数据库轮询
这种方式一般适合小型项目,通过启一个定时任务,周期性的扫描数据库,获取订单信息,然后执行状态更新或删除操作
常见的基本都是基于quartz
实现定时轮询
伪代码:
1.引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
2.配置定时任务
注意在启动类加上@EnableScheduling
注解,否则无法开启定时任务
这里只是简单写个quartz
伪代码,实际开发中定时任务可能会比较复杂一些,详细可参看《Quartz基于配置实现动态定时任务执行》
@Component
@Slf4j
public class CancelTheOrderTask {
@Scheduled(cron = "0/10 * * * * ?}")
public void run(){
log.info("开始扫描数据库订单信息......");
log.info("修改或删除超时未支付定时信息......");
log.info("结束扫描数据库订单信息......");
}
}
4.输出信息
2022-09-03 13:33:00.013 INFO 23656 --- [ scheduling-1] c.capitek.controller.CancelTheOrderTask : 开始扫描数据库订单信息......
2022-09-03 13:33:00.014 INFO 23656 --- [ scheduling-1] c.capitek.controller.CancelTheOrderTask : 修改或删除超时未支付定时信息......
2022-09-03 13:33:00.014 INFO 23656 --- [ scheduling-1] c.capitek.controller.CancelTheOrderTask : 结束扫描数据库订单信息......
2022-09-03 13:33:10.008 INFO 23656 --- [ scheduling-1] c.capitek.controller.CancelTheOrderTask : 开始扫描数据库订单信息......
2022-09-03 13:33:10.009 INFO 23656 --- [ scheduling-1] c.capitek.controller.CancelTheOrderTask : 修改或删除超时未支付定时信息......
2022-09-03 13:33:10.010 INFO 23656 --- [ scheduling-1] c.capitek.controller.CancelTheOrderTask : 结束扫描数据库订单信息......
优点:
-
简单易行,quartz本身也支持集群操作
缺点:
-
存在延迟,试想你如果每隔30分钟轮询一次,一个订单刚好过29分钟,那你就要下个30分钟才可以处理这个订单信息 -
如果每秒轮询一次,对服务器消耗太大 -
而且随着你订单业务量的加大,也会增加数据库的压力
所以这种方式不常见,也不常用,但它也是方案之一
JDK自带的延迟队列
之前有篇文章专门介绍了一下JDK中几种常见的队列,里面就有介绍DelayQueue
,详细可参考《如何优雅的使用Java队列》,今天以代码的方式给大家做个简单实现
注意:队列中的元素必须实现Delayed
接口,才可以使用DelayQueue
伪代码:
public class OrderDelay implements Delayed {
//订单号
private String orderId;
//订单超时时间
private long timeout;
public OrderDelay(String orderId, long timeout) {
this.orderId = orderId;
this.timeout = timeout + System.nanoTime();
}
public int compareTo(Delayed other) {
if (other == this) return 0;
OrderDelay t = (OrderDelay) other;
long d = (getDelay(TimeUnit.NANOSECONDS) - t.getDelay(TimeUnit.NANOSECONDS));
return (d == 0) ? 0 : ((d < 0) ? -1 : 1);
}
// 返回距离你自定义的超时时间还剩多少
public long getDelay(TimeUnit unit) {
return unit.convert(timeout - System.nanoTime(),TimeUnit.NANOSECONDS);
}
public void print() {
System.out.println(orderId+"编号的订单修改或删除......");
}
public static void main(String[] args) {
DelayQueue<OrderDelay> queue = new DelayQueue<OrderDelay>();
long start = System.currentTimeMillis();
//这里做个简单模拟,实际可以提交订单时放入延迟队列中
for(int i = 0;i<5;i++){
//延迟10秒取出
queue.put(new OrderDelay("0000-"+i, TimeUnit.NANOSECONDS.convert(10,TimeUnit.SECONDS)));
try {
queue.take().print();
System.out.println((System.currentTimeMillis()-start) + " ms");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
输出信息
0000-0编号的订单修改或删除......10013 ms
0000-1编号的订单修改或删除......20022 ms
0000-2编号的订单修改或删除......30036 ms
0000-3编号的订单修改或删除......40048 ms
0000-4编号的订单修改或删除......50063 ms
优点:
-
效率高,任务触发时间延迟低
缺点:
-
集群化扩展比较麻烦 -
而且随着你订单业务量的加大,会增加服务器压力,有可能出现缓存溢出
时间轮算法
网上看到一篇基于Netty
的HashedWheelTimer
的实现,这里也介绍一下
JAVA本身提供了java.util.Timer和java.util.concurrent.ScheduledThreadPoolExecutor等多种Timer工具,但是这些工具在执行效率上面还是有些缺陷,比如Timer,想要提交任务的话需要创建一个TimerTask类,于是netty提供了HashedWheelTimer,一个优化的Timer类
HashedWheelTimer本质是一种类似延迟任务队列的实现,适用于对时效性不高的,可快速执行的,大量这样的“小”任务,能够做到高性能,低消耗
伪代码:
1.引入依赖
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.24.Final</version>
</dependency>
2.配置定时任务
HashedWheelTimer
public class MyTimerTask implements TimerTask {
@Override
public void run(Timeout timeout) throws Exception {
System.out.println("修改或删除超时未支付定时信息......");
}
public static void main(String[] args) {
MyTimerTask myTimerTask = new MyTimerTask();
Timer timer = new HashedWheelTimer();
timer.newTimeout(myTimerTask, 5, TimeUnit.SECONDS);
timer.newTimeout(myTimerTask, 10, TimeUnit.SECONDS);
}
修改或删除超时未支付定时信息......2022-09-03T14:54:22.440
修改或删除超时未支付定时信息......2022-09-03T14:54:27.380
}
java.util.Timer
public class MyTimerTask2 extends TimerTask {
@Override
public void run() {
System.out.println("修改或删除超时未支付定时信息......"+ LocalDateTime.now());
}
public static void main(String[] args) {
MyTimerTask2 timerTask = new MyTimerTask2();
Timer timer = new Timer();
timer.schedule(timerTask, 1000,5000);
timer.schedule(timerTask, 1000,10000); //会报Exception in thread "main" java.lang.IllegalStateException: Task already scheduled or cancelled 异常
}
}
Exception in thread "main" java.lang.IllegalStateException: Task already scheduled or cancelled
at java.util.Timer.sched(Unknown Source)
at java.util.Timer.schedule(Unknown Source)
at com.capitek.controller.MyTimerTask2.main(MyTimerTask2.java:17)
修改或删除超时未支付定时信息......2022-09-03T15:09:44.947
优点:
-
效率高,任务触发时间延迟时间比delayQueue低,代码复杂度比delayQueue低
缺点:
-
集群化扩展比较麻烦 -
而且随着你订单业务量的加大,会增加服务器压力,有可能出现缓存溢出
Redis
redis的zset
zset是一个有序集合,每一个元素(member)都关联了一个score,通过score排序来取集合中的值
伪代码:
@GetMapping("/test")
public void test() throws Exception {
RedisTemplate redisTemplate = redisService.getRedisTemplate();
for (int i = 0; i < 5; i++) {
redisTemplate.opsForZSet().add("OrderId","00000"+i,LocalDateTime.now().plusSeconds(5+i).toInstant(ZoneOffset.of("+8")).toEpochMilli());
}
listenDelayLoop();
}
/**
* 监听延迟消息
*/
public void listenDelayLoop() {
RedisTemplate redisTemplate = redisService.getRedisTemplate();
while (true) {
// 获取一个到点的消息
Set<String> set = redisTemplate.opsForZSet().rangeByScore("OrderId", 0, LocalDateTime.now().toInstant(ZoneOffset.of("+8")).toEpochMilli(),0,1);
// 如果没有,就等等
if (set.isEmpty()) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 继续执行
continue;
}
// 获取具体消息的key
String it = set.iterator().next();
// 删除成功
if (redisTemplate.opsForZSet().remove("OrderId", it) > 0) {
// 拿到任务
System.out.println("消息到期"+it+",时间为"+ LocalDateTime.now());
}
}
}
输出:
消息到期000000,时间为2022-09-03T15:46:18.165
消息到期000001,时间为2022-09-03T15:46:19.172
消息到期000002,时间为2022-09-03T15:46:20.197
消息到期000003,时间为2022-09-03T15:46:21.206
消息到期000004,时间为2022-09-03T15:46:22.225
过期监听
Redis 自动过期的实现方式是:定时任务离线扫描并删除部分过期键;在访问键时惰性检查是否过期并删除过期键。
Redis 从未保证会在设定的过期时间立即删除并发送过期通知。实际上,过期通知晚于设定的过期时间数分钟的情况也比较常见。
此外键空间通知采用的是发送即忘(fire and forget)策略,并不像消息队列一样保证送达。当订阅事件的客户端会丢失所有在断线期间所有分发给它的事件。
这是一种比定时扫描数据库更 “LOW” 的解决方案
伪代码:
1.检查是否开启redis过期监听,修改redis.conf 文件
notify-keyspace-events Ex #修改为EX
2.创建RedisKeyExpirationListener
package com.test.springbootredislisten.listen;
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 org.springframework.stereotype.Component;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import java.io.IOException;
@Component
@Slf4j
public class RedisKeyExpirationListener extends KeyExpirationEventMessageListener {
public RedisKeyExpirationListener(RedisMessageListenerContainer listenerContainer) {
super(listenerContainer);
}
/**
* Redis失效事件 key
*
* @param message
* @param pattern
*/
@Override
public void onMessage(Message message, byte[] pattern) {
String expiraKey = message.toString();
System.out.println(expiraKey);
//这里写业务逻辑
}
}
3.创建RedisListenerConfig
package com.test.springbootredislisten.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
@Configuration
public class RedisListenerConfig {
@Bean
RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
return container;
}
}
4.创建RedisConfig
package com.test.springbootredislisten.config;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.time.Duration;
@EnableCaching
@Configuration
public class RedisConfig extends CachingConfigurerSupport {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
RedisSerializer<String> redisSerializer = new StringRedisSerializer();
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
template.setConnectionFactory(factory);
//key序列化方式
template.setKeySerializer(redisSerializer);
//value序列化
template.setValueSerializer(jackson2JsonRedisSerializer);
//value hashmap序列化
template.setHashValueSerializer(jackson2JsonRedisSerializer);
return template;
}
@Bean
public CacheManager cacheManager(RedisConnectionFactory factory) {
RedisSerializer<String> redisSerializer = new StringRedisSerializer();
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
//解决查询缓存转换异常的问题
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
// 配置序列化(解决乱码的问题),过期时间600秒
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofSeconds(600))
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer))
.disableCachingNullValues();
RedisCacheManager cacheManager = RedisCacheManager.builder(factory)
.cacheDefaults(config)
.build();
return cacheManager;
}
}
消息队列
这里不再详细介绍,可以参考《一文让你了解rabbitmq》,文中附有代码
号外!号外!
如果这篇文章对你有所帮助,或者有所启发的话,帮忙点赞、在看、转发、收藏,你的支持就是我坚持下去的最大动力!
原文始发于微信公众号(一安未来):面试官:生成订单30分钟未支付,则自动取消,该怎么实现
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/44868.html