【代码模板】延时任务
Kiml Lv5
  • 前言

业务场景:
1、生成订单 30 分钟未支付,则自动取消(延时任务)
2、生成订单 60 秒后,给用户发短信(延时任务)

  • 更新
1
24-08-18 初始记录(从原先的笔记进行搬运)

定时任务与延时任务的区别

  • 定时任务有明确的触发时间,延时任务没有

  • 定时任务有执行周期,而延时任务在某事件触发后一段时间内执行,没有执行周期

  • 定时任务一般执行的是批处理操作是多个任务,而延时任务一般是单个任务

解决思路

数据库定时轮询(定时任务)

通常在小型项目中使用,即通过一个线程定时的去扫描数据库,通过订单时间来判断是否有超时的订单,然后进行 update 或 delete 等操作。

实现

通过 springBoot 中的@Schedule 进行实现

步骤

  1. 在启动类上添加注解@EnableScheduling

1
2
3
4
5
6
7
@SpringBootApplication  
@EnableScheduling
public class ScheduleAppcation {
public static void main(String[] args) {
SpringApplication.run(ScheduleAppcation.class, args);
}
}
  1. 在目标类上添加注解

1
2
3
4
5
// 目标任务执行完后,延迟10s执行。
@Scheduled(fixedDelay = 10 * 1000)
public void testJob01() {
...
}
1
2
3
4
5
// cron表达式
@Scheduled(cron = "0 0 6 * * ?")
public void testJob01() {
...
}

优缺点

  • 存在延时,定时任务默认是单线程执行,前一个任务阻塞会影响后一个任务的执行

  • 数据库数据量大的情况下,扫描损耗巨大

JDK 的延迟队列

该方案是利用 JDK 自带的 DelayQueue 来实现,这是一个无界阻塞队列,该队列只有在延迟期满的时候才能从中获取元素,放入 DelayQueue 中的对象,是必须实现 Delayed 接口的。

DelayQueue 属于排序队列,它的特殊之处在于队列的元素必须实现 Delayed 接口,该接口需要实现 compareTo 和 getDelay 方法

getDelay 方法:获取元素在队列中的剩余时间,只有当剩余时间为 0 时元素才可以出队列。

compareTo 方法:用于排序,确定元素出队列的顺序。

实现

利用 JDK 自带的 DelayQueue 来实现

步骤

  1. 在测试包 jdk 下创建延迟任务元素对象 DelayedTask,实现 compareTo 和 getDelay 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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 );
}
}
  1. 在 main 方法中创建 DelayQueue 并向延迟队列中添加三个延迟任务

  2. 循环的从延迟队列中拉取任务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public static void main(String[] args) {
DelayQueue<DelayedTask> queue = new DelayQueue<DelayedTask>();

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();
}
}
}

优缺点

  • 数据存储在内存中,容易出现 OOM(Out Of Memory)异常

  • 服务器宕机后,内存中的数据容易丢失

Redis 缓存

实现

利用 Redis 中 Key 的过期时间

步骤

  1. 给 Redis 中 Key 设置过期时间

  2. 监听 Redis 中 Key 过期事件

  3. 获取过期 Key 对应的值进行消费

    • 过期 Key 拿不到值
      • 可以把信息存储到 Key 上(监听事件可以获取到即将过期的 key,可以将文章 id 存储到 redis 中)
      • 存储一份不过期的对应 Key,在 Key 过期时获取这个不过期 Key 取值再删除

优缺点

  • 若 Redis 监听过期 Key 的微服务是多个集群时,只能有一个微服务处理,需要引入分布式锁,性能会下降。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package com.heima.common.redislock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
public class RedisLockImpl implements RedisLock {
@Autowired
private StringRedisTemplate stringRedisTemplate;
private ThreadLocal<String> threadLocal = new ThreadLocal<String>();
private ThreadLocal<Integer> threadLocalInteger = new ThreadLocal<Integer>();

@Override
public boolean tryLock(String key, long timeout, TimeUnit unit) {
Boolean isLocked = false;
if (threadLocal.get() == null) {
String uuid = UUID.randomUUID().toString();
threadLocal.set(uuid);
isLocked = stringRedisTemplate.opsForValue().setIfAbsent(key, uuid, timeout, unit);
} else {
isLocked = true;
}
// 重入次数加1
if (isLocked) {
Integer count = threadLocalInteger.get() == null ? 0 : threadLocalInteger.get();
threadLocalInteger.set(count++);
}
return isLocked;
}
@Override
public void releaseLock(String key) {
// 判断当前线程所对应的uuid是否与Redis对应的uuid相同,再执行删除锁操作
if (threadLocal.get().equals(stringRedisTemplate.opsForValue().get(key))) {
Integer count = threadLocalInteger.get();
// 计数器减为0时才能释放锁
if (count == null || --count <= 0) {
stringRedisTemplate.delete(key);
}
}
}
}

使用 MQ 实现延时任务

实现

内链:[[消息队列的选型与优缺点]]
外链:消息队列的选型与优缺点

优缺点

  • 高效,可以利用 MQ 的分布式特性轻易的进行横向扩展,消息支持持久化增加了可靠性。

 评论
评论插件加载失败
正在加载评论插件
由 Hexo 驱动 & 主题 Keep
访客数 访问量