众所周知Redis是一个基于内存操作的高效的键值对存储数据库,Redis之所以如此高效主要在于他基于内存操作、高效的数据结构以及合理的线程模型。在Redis的使用中Redis作为延迟队列是他的一个重要应用场景
目录
前言
一、什么是延迟任务?
二、技术对比
1.定时任务
2.DelayQueue
3.RabbitMQ实现延迟任务
(一)生产者
(二)消费者
4.redis实现
1.首先需要创建两张表:任务表 和任务执行日志记录表
2.添加延迟任务
3.取消任务
4.按照类型和优先级拉取任务(消费)
在上一篇文章 Java中的redis介绍以及运用场景-CSDN博客中说到redis不仅仅可以用作缓存,可以用作分布式锁 由于redis高效的数据结构,redis可以用来做消息队列使用,以下就是关于怎么用redis来作为延迟队列的说明
定时任务:有固定周期的,有明确的触发时间
延迟队列:没有固定的开始时间,它常常是由一个事件触发的,而在这个事件触发之后的一段时间内触发另一个事件,任务可以立即执行,也可以延迟
应用场景:
场景一:订单下单之后30分钟后,如果用户没有付钱,则系统自动取消订单;如果期间下单成功,任务取消(这也是比较常用的场景,为了防止库存一直被锁定)
场景二:接口对接出现网络问题,1分钟后重试,如果失败,2分钟重试,直到出现阈值终止
比如现在需要在30分钟后去取消未支付的订单,其实实现方式有很多种
1.可以通过定时任务去刷表的形式去处理这个业务 但是这也是最垃圾的一种方式 在消耗系统资源内存方面以及数据库 等方面都有很大的消耗
2.可以使用redis的延迟队列去实现
3.可以使用DelayQueue队列实现
4.可以使用 redid key实效监听来实现
5.使用mq组件来实现 但是这个需要有一定的条件 毕竟加了mq也是需要维护的像一般的小公司其实用的很少
这个比较简单就是设置一个定任务 每隔一秒钟去刷数据 判断订单的创建时间和当前时间来判断订单是否已到自动取消的时间
JDK自带DelayQueue 是一个支持延时获取元素的阻塞队列, 内部采用优先队列 PriorityQueue 存储元素,同时元素必须实现 Delayed 接口;在创建元素时可以指定多久才可以从队列中获取当前元素,只有在延迟期满时才能从队列中提取元素
DelayQueue属于排序队列,它的特殊之处在于队列的元素必须实现Delayed接口,该接口需要实现compareTo和getDelay方法
getDelay方法:获取元素在队列中的剩余时间,只有当剩余时间为0时元素才可以出队列。
compareTo方法:用于排序,确定元素出队列的顺序。
实现:
1:在测试包jdk下创建延迟任务元素对象DelayedTask,实现compareTo和getDelay方法,
2:在main方法中创建DelayQueue并向延迟队列中添加三个延迟任务,
3:循环的从延迟队列中拉取任务
public class DelayedTask implements Delayed{
// 任务的执行时间
private int executeTime = 0;
public DelayedTask(int delay){
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.SECOND,delay);
this.executeTime = (int)(calendar.getTimeInMillis() /1000 );
}
/**
* 元素在队列中的剩余时间
* @param unit
* @return
*/
@Override
public long getDelay(TimeUnit unit) {
Calendar calendar = Calendar.getInstance();
return executeTime - (calendar.getTimeInMillis()/1000);
}
/**
* 元素排序
* @param o
* @return
*/
@Override
public int compareTo(Delayed o) {
long val = this.getDelay(TimeUnit.NANOSECONDS) - o.getDelay(TimeUnit.NANOSECONDS);
return val == 0 ? 0 : ( val < 0 ? -1: 1 );
}
public static void main(String[] args) {
DelayQueue queue = new DelayQueue();
queue.add(new DelayedTask(5));
queue.add(new DelayedTask(10));
queue.add(new DelayedTask(15));
System.out.println(System.currentTimeMillis()/1000+" start consume ");
while(queue.size() != 0){
DelayedTask delayedTask = queue.poll();
if(delayedTask !=null ){
System.out.println(System.currentTimeMillis()/1000+" cosume task");
}
//每隔一秒消费一次
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
DelayQueue实现完成之后思考一个问题:
使用线程池或者原生DelayQueue程序挂掉之后,任务都是放在内存,需要考虑未处理消息的丢失带来的影响,如何保证数据不丢失,需要持久化(磁盘)
想了解具体的源码实现可以看这篇博客 觉得写的还蛮好的 延迟队列DelayQueue原理-CSDN博客文章浏览阅读1.3w次,点赞23次,收藏63次。前言什么是DelayQueue(延时队列)DelayQueue 是一个通过PriorityBlockingQueue实现延迟获取元素的无界队列无界阻塞队列,其中添加进该队列的元素必须实现Delayed接口(指定延迟时间),而且只有在延迟期满后才能从中提取元素。什么是PriorityBlockingQueue(优先队列)PriorityBlockingQueue是一个支持优先级的无界阻塞队列,队列的元素默认情况下元素采用自然顺序升序排列,或者根据构造队列时提供的 Comparator 进行排序,具体取_delayqueuehttps://blog.csdn.net/c15158032319/article/details/118636233
TTL:Time To Live (消息存活时间)
死信队列:Dead Letter Exchange(死信交换机),当消息成为Dead message后,可以重新发送另一个交换机(死信交换机)
将订单存入redis中
for (OrderEntity order : orders) {
//1.将订单存入信息Redis
RedisUtils.setObject(RabbitTemplateConfig.ORDER_PREFIX + order.getId(), order);
// 2.向RabbitMQ异步投递消息 /
rabbitTemplate.convertAndSend(RabbitmqConfig.DELAY_EXCHANGE_NAME, RabbitmqConfig.DELAY_KEY, order, RabbitUtils.setDelay(30000), RabbitUtils.correlationData(order.getId()));
}
生产者可靠投递消息
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
if (correlationData == null) {
return;
}
String key = ORDER_PREFIX + correlationData.getId();
if (ack) {
/* 如果消息投递成功,则删除Redis中订单数据,回收内存 */
RedisUtils.deleteObject(key);
} else {
/* 从Redis中读取订单数据,重新投递 */
OrderEntity order = RedisUtils.getObject(key, OrderEntity.class);
/* 重新投递消息 */
rabbitTemplate.convertAndSend(RabbitmqConfig.DELAY_EXCHANGE_NAME, RabbitmqConfig.DELAY_KEY, order, RabbitUtils.setDelay(30000), RabbitUtils.correlationData(order.getOrderId()));
}
}
消费者端手动确认,避免消息丢失;失败自动重试。
@RabbitListener(queues = RabbitmqConfig.DELAY_QUEUE_NAME)
public void consumeNode01(Channel channel, Message message, OrderEntity order) throws IOException {
if (Objects.equals(0, order.getOrderStatus())) {
/* 修改订单状态,设置为关闭状态 */
orderService.updateById(new OrderEntity(order.getOrderId(), -1));
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
log.info(String.format("消费者节点01消费编号为【%s】的消息", order.getId()));
}
}
消费者可靠消费应至少开启两个及以上应用,确保消息队列中不积压消息。
zset数据类型的去重有序(分数排序)特点进行延迟。例如:时间戳作为score进行排序
问题思路:
1.为什么任务需要存储在数据库中?
延迟任务是一个通用的服务,任何需要延迟得任务都可以调用该服务,需要考虑数据持久化的问题,存储数据库中是一种数据安全的考虑。
2.为什么redis中使用两种数据类型,list和zset?
效率问题,算法的时间复杂度
3.在添加zset数据的时候,为什么不需要预加载?
任务模块是一个通用的模块,项目中任何需要延迟队列的地方,都可以调用这个接口,要考虑到数据量的问题,如果数据量特别大,为了防止阻塞,只需要把未来几分钟要执行的数据存入缓存即可。
@FeignClient("leadnews-schedule")
public interface IScheduleClient {
/**
* 添加延迟任务
* @param task
* @return
*/
@PostMapping("/api/v1/task/add")
public ResponseResult addTask(@RequestBody Task task);
/**
* 取消任务
* @param taskId
* @return
*/
@GetMapping("/api/v1/task/{taskId}")
public ResponseResult cancelTask(@PathVariable("taskId") long taskId);
/**
* 按照类型和优先级拉取任务
* @param type
* @param priority
* @return
*/
@GetMapping("/api/v1/task/{type}/{priority}")
public ResponseResult poll(@PathVariable("type") int type,@PathVariable("priority") int priority);
}
@RestController
public class ScheduleClient implements IScheduleClient {
@Autowired
private TaskService taskService;
/**
* 添加延迟任务
*
* @param task
* @return
*/
@PostMapping("/api/v1/task/add")
public ResponseResult addTask(@RequestBody Task task) {
return ResponseResult.okResult(taskService.addTask(task));
}
/**
* 取消任务
*
* @param taskId
* @return
*/
@GetMapping("/api/v1/task/{taskId}")
public ResponseResult cancelTask(@PathVariable("taskId") long taskId){
return ResponseResult.okResult(taskService.cancelTask(taskId));
}
/**
* 按照类型和优先级拉取任务
*
* @param type
* @param priority
* @return
*/
@GetMapping("/api/v1/task/{type}/{priority}")
public ResponseResult poll(@PathVariable("type") int type,@PathVariable("priority") int priority) {
return ResponseResult.okResult(taskService.poll(type,priority));
}
}
在创建订单的时候把订单放入任务队列之中
在用户支付完成之后可以把任务队列中的任务取消 并删除redis中的该订单任务
/**
* 消费任务,
*/
@Scheduled(fixedRate = 1000)
@Override
public void scanNewsByTask() {
log.info("消费任务,取消超时支付订单");
ResponseResult responseResult = scheduleClient.poll(TaskTypeEnum.SCAN_TIME.getTaskType(), TaskTypeEnum.SCAN_TIME.getPriority());
if(responseResult.getCode().equals(200) && responseResult.getData() != null){
//做逻辑处理
}
}