Запланированные задачи особенно распространены в реальной разработке, например, платформа электронной коммерции автоматически отменяет неоплаченные заказы через 30 минут, а также агрегацию и резервное копирование данных рано утром и т. д., и все это необходимо реализовать с помощью запланированных задач. так что давайте посмотрим на эту статью.Простейшая реализация задач на время.
ТОП 1: Таймер
Timer — это класс выполнения задач синхронизации, поставляемый с JDK. Любой проект может напрямую использовать Timer для реализации задач синхронизации, поэтому преимущество Timer заключается в простоте использования. Его код реализации выглядит следующим образом:
public class MyTimerTask {
public static void main(String[] args) {
// 定义一个任务
TimerTask timerTask = new TimerTask() {
@Override
public void run() {
System.out.println("Run timerTask:" + new Date());
}
};
// 计时器
Timer timer = new Timer();
// 添加执行任务(延迟 1s 执行,每 3s 执行一次)
timer.schedule(timerTask, 1000, 3000);
}
}
Результат выполнения программы следующий:
Run timerTask: Пн, 17 августа, 21:29:25 CST 2020
Run timerTask: Пн, 17 августа, 21:29:28 CST 2020
Run timerTask: Пн, 17 августа, 21:29:31 CST 2020
Анализ недостатков таймера
Хотя класс Timer удобен для реализации задач с синхронизацией, при его использовании необходимо обратить внимание на следующие проблемы.
Проблема 1: длительное время выполнения задачи влияет на другие задачи
Когда время выполнения задачи слишком велико, это повлияет на планирование других задач, как показано в следующем коде:
public class MyTimerTask {
public static void main(String[] args) {
// 定义任务 1
TimerTask timerTask = new TimerTask() {
@Override
public void run() {
System.out.println("进入 timerTask 1:" + new Date());
try {
// 休眠 5 秒
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Run timerTask 1:" + new Date());
}
};
// 定义任务 2
TimerTask timerTask2 = new TimerTask() {
@Override
public void run() {
System.out.println("Run timerTask 2:" + new Date());
}
};
// 计时器
Timer timer = new Timer();
// 添加执行任务(延迟 1s 执行,每 3s 执行一次)
timer.schedule(timerTask, 1000, 3000);
timer.schedule(timerTask2, 1000, 3000);
}
}
Результат выполнения программы следующий:
Введите timerTask 1: понедельник, 17 августа, 21:44:08 CST 2020
Run timerTask 1: Пн, 17 августа, 21:44:13 CST 2020
Run timerTask 2: Пн, 17 августа, 21:44:13 CST 2020
Введите timerTask 1: Пн, 17 августа, 21:44:13 CST 2020
Run timerTask 1: Пн, 17 августа, 21:44:18 CST 2020
Введите timerTask 1: понедельник, 17 августа, 21:44:18 CST 2020
Run timerTask 1: Пн, 17 августа, 21:44:23 CST 2020
Run timerTask 2: понедельник, 17 августа, 21:44:23 CST 2020
Введите timerTask 1: понедельник, 17 августа, 21:44:23 CST 2020
Как видно из приведенных выше результатов, когда задача 1 выполняется дольше установленного интервала, задача 2 также будет откладывать выполнение. **Изначально временной интервал выполнения задачи 1 и задачи 2 составляет 3 с, но поскольку задача 1 выполняется в течение 5 с, временной интервал выполнения задачи 2 также становится равным 10 с (что не соответствует исходному времени).
Проблема 2: Исключение задачи влияет на другие задачи
При использовании класса Timer для реализации задач по времени, когда задача выдает исключение, другие задачи также завершаются, как показано в следующем коде:
public class MyTimerTask {
public static void main(String[] args) {
// 定义任务 1
TimerTask timerTask = new TimerTask() {
@Override
public void run() {
System.out.println("进入 timerTask 1:" + new Date());
// 模拟异常
int num = 8 / 0;
System.out.println("Run timerTask 1:" + new Date());
}
};
// 定义任务 2
TimerTask timerTask2 = new TimerTask() {
@Override
public void run() {
System.out.println("Run timerTask 2:" + new Date());
}
};
// 计时器
Timer timer = new Timer();
// 添加执行任务(延迟 1s 执行,每 3s 执行一次)
timer.schedule(timerTask, 1000, 3000);
timer.schedule(timerTask2, 1000, 3000);
}
}
Результат выполнения программы следующий:
Введите timerTask 1: понедельник, 17 августа, 22:02:37 CST 2020
Exception in thread "Timer-0" java.lang.ArithmeticException: / by zero
в com.example.MyTimerTask$1.run(MyTimerTask.java:21)
в java.util.TimerThread.mainLoop(Timer.java:555)
в java.util.TimerThread.run(Timer.java:505)
Process finished with exit code 0
Сводка по таймеру
Преимущество класса Timer для реализации временных задач заключается в удобстве, поскольку это самоопределяемая временная задача JDK, но недостатком является то, что если время выполнения задачи слишком велико или выполнение задачи ненормальное, это повлияет на планирование другие задачи, поэтому рекомендуется использовать его с осторожностью в производственной среде.
ТОП 2: ScheduledExecutorService
ScheduledExecutorService также является API, который поставляется с JDK 1.5, мы можем использовать его для реализации функции запланированных задач, то естьScheduledExecutorService может реализовать все функции класса Timer и решить все проблемы класса Timer..
Пример кода ScheduledExecutorService, реализующего запланированные задачи, выглядит следующим образом:
public class MyScheduledExecutorService {
public static void main(String[] args) {
// 创建任务队列
ScheduledExecutorService scheduledExecutorService =
Executors.newScheduledThreadPool(10); // 10 为线程数量
// 执行任务
scheduledExecutorService.scheduleAtFixedRate(() -> {
System.out.println("Run Schedule:" + new Date());
}, 1, 3, TimeUnit.SECONDS); // 1s 后开始执行,每 3s 执行一次
}
}
Результат выполнения программы следующий:
Расписание забега: пн, 17 августа, 21:44:23 CST 2020
Расписание забега: пн, 17 августа, 21:44:26 CST 2020
Расписание забега: пн, 17 августа, 21:44:29 CST 2020
Проверка надежности ScheduledExecutorService
① Тест выполнения задачи по тайм-ауту
ScheduledExecutorService может устранить недостаток соответствующего влияния между задачами таймера., прежде всего, давайте проверим, не слишком ли велико время выполнения задачи, и это повлияет на другие задачи.Код теста выглядит следующим образом:
public class MyScheduledExecutorService {
public static void main(String[] args) {
// 创建任务队列
ScheduledExecutorService scheduledExecutorService =
Executors.newScheduledThreadPool(10);
// 执行任务 1
scheduledExecutorService.scheduleAtFixedRate(() -> {
System.out.println("进入 Schedule:" + new Date());
try {
// 休眠 5 秒
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Run Schedule:" + new Date());
}, 1, 3, TimeUnit.SECONDS); // 1s 后开始执行,每 3s 执行一次
// 执行任务 2
scheduledExecutorService.scheduleAtFixedRate(() -> {
System.out.println("Run Schedule2:" + new Date());
}, 1, 3, TimeUnit.SECONDS); // 1s 后开始执行,每 3s 执行一次
}
}
Результат выполнения программы следующий:
Расписание 2: пн, 17 августа, 11:27:55 по центральному поясному времени 2020 г.
Введите расписание: пн, 17 августа, 11:27:55 CST 2020
Расписание забегов2: пн, 17 августа, 11:27:58 CST 2020
Расписание забега: пн, 17 августа, 11:28:00 по центральному поясному времени 2020 г.
Введите расписание: пн, 17 августа, 11:28:00 CST 2020
Расписание забегов2: Пн, 17 августа, 11:28:01 CST 2020
Расписание забегов2: пн, 17 августа, 11:28:04 CST 2020
Из приведенных выше результатов видно, что когда время выполнения задачи 1 превышает частоту выполнения 3 с, это не влияет на нормальное выполнение задачи 2.Следовательно, использование ScheduledExecutorService позволяет избежать влияния длительного времени выполнения задачи на другие задачи..
② Тест на отклонение от нормы
Затем давайте проверим, повлияет ли ScheduledExecutorService на другие задачи, когда задача ненормальна.Код теста выглядит следующим образом:
public class MyScheduledExecutorService {
public static void main(String[] args) {
// 创建任务队列
ScheduledExecutorService scheduledExecutorService =
Executors.newScheduledThreadPool(10);
// 执行任务 1
scheduledExecutorService.scheduleAtFixedRate(() -> {
System.out.println("进入 Schedule:" + new Date());
// 模拟异常
int num = 8 / 0;
System.out.println("Run Schedule:" + new Date());
}, 1, 3, TimeUnit.SECONDS); // 1s 后开始执行,每 3s 执行一次
// 执行任务 2
scheduledExecutorService.scheduleAtFixedRate(() -> {
System.out.println("Run Schedule2:" + new Date());
}, 1, 3, TimeUnit.SECONDS); // 1s 后开始执行,每 3s 执行一次
}
}
Результат выполнения программы следующий:
Введите расписание: понедельник, 17 августа, 22:17:37 CST 2020
Расписание 2: пн, 17 августа, 22:17:37 CST 2020
Расписание забегов2: пн, 17 августа, 22:17:40 CST 2020
Расписание 2: пн, 17 августа, 22:17:43 по центральному поясному времени 2020 г.
Из вышеприведенных результатов видно, чтоКогда в задаче 1 возникает исключение, оно не повлияет на выполнение задачи 2..
Сводка ScheduledExecutorService
В автономной производственной среде для выполнения запланированных задач рекомендуется использовать ScheduledExecutorService.Это API, который поставляется с JDK 1.5 и более поздними версиями, поэтому его удобнее использовать, а использование ScheduledExecutorService для выполнения задач не вызовет взаимного влияния. между задачами.
ТОП 3: Весеннее задание
Если вы используете платформу Spring или Spring Boot, вы можете напрямую использовать временные задачи, поставляемые с Spring Framework.Используя приведенные выше два метода реализации задач на время, трудно реализовать задачи на время с определенными настройками времени, например, когда нам нужно выполнять задачу каждую пятницу., но если вы используете Spring Task, вы можете легко выполнить это требование.
Взяв в качестве примера Spring Boot, для реализации задачи по времени требуется всего два шага:
- Запускать задачи на время;
- Добавьте запланированные задачи.
Конкретные этапы реализации следующие.
① Запустите запланированное задание
Чтобы запустить запланированную задачу, вам нужно только объявить ее в классе запуска Spring Boot.@EnableScheduling
Вот и все, код реализации такой:
@SpringBootApplication
@EnableScheduling // 开启定时任务
public class DemoApplication {
// do someing
}
② Добавить запланированное задание
Добавление задач на время нужно только использовать@Scheduled
Достаточно аннотации.Если есть несколько запланированных задач, можно создать несколько задач.@Scheduled
Метод аннотации аннотации, пример кода выглядит следующим образом:
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
@Component // 把此类托管给 Spring,不能省略
public class TaskUtils {
// 添加定时任务
@Scheduled(cron = "59 59 23 0 0 5") // cron 表达式,每周五 23:59:59 执行
public void doTask(){
System.out.println("我是定时任务~");
}
}
Примечание. Запланированные задачи запускаются автоматически без ручного вмешательства, что означает, что Spring Boot автоматически загрузит и выполнит запланированные задачи после запуска.
Cron-выражения
Реализация Spring Task должна использовать выражение cron для объявления частоты и правил выполнения.Выражение cron состоит из 6 или 7 цифр (последняя цифра может быть опущена), и каждый бит разделен пробелом, и каждый бит идет слева налево, смысл правого таков:
Оба знака * и ? означают совпадение всегда.
Выражение cron генерирует адрес онлайн:cron.qqe2.com/
Расширение знаний: Распределенные задачи синхронизации
Все вышеперечисленные методы касаются реализации задач синхронизации на одном компьютере.Если это распределенная среда, для реализации задач синхронизации можно использовать Redis.
Использование RedisМетоды реализации отложенных задач можно условно разделить на две категории: способ через ZSet и способ уведомления о пространстве ключей..
① Реализация ZSet
Идея реализации задач по времени через ZSet состоит в том, чтобы хранить задачи по времени в коллекции ZSet и хранить время истечения в поле Score ZSet, а затем использовать беспроводную петлю, чтобы определить, есть ли задачи по времени, которые необходимо выполняться в текущее время, а затем выполняться, конкретный код реализации выглядит следующим образом:
import redis.clients.jedis.Jedis;
import utils.JedisUtils;
import java.time.Instant;
import java.util.Set;
public class DelayQueueExample {
// zset key
private static final String _KEY = "myTaskQueue";
public static void main(String[] args) throws InterruptedException {
Jedis jedis = JedisUtils.getJedis();
// 30s 后执行
long delayTime = Instant.now().plusSeconds(30).getEpochSecond();
jedis.zadd(_KEY, delayTime, "order_1");
// 继续添加测试数据
jedis.zadd(_KEY, Instant.now().plusSeconds(2).getEpochSecond(), "order_2");
jedis.zadd(_KEY, Instant.now().plusSeconds(2).getEpochSecond(), "order_3");
jedis.zadd(_KEY, Instant.now().plusSeconds(7).getEpochSecond(), "order_4");
jedis.zadd(_KEY, Instant.now().plusSeconds(10).getEpochSecond(), "order_5");
// 开启定时任务队列
doDelayQueue(jedis);
}
/**
* 定时任务队列消费
* @param jedis Redis 客户端
*/
public static void doDelayQueue(Jedis jedis) throws InterruptedException {
while (true) {
// 当前时间
Instant nowInstant = Instant.now();
long lastSecond = nowInstant.plusSeconds(-1).getEpochSecond(); // 上一秒时间
long nowSecond = nowInstant.getEpochSecond();
// 查询当前时间的所有任务
Set<String> data = jedis.zrangeByScore(_KEY, lastSecond, nowSecond);
for (String item : data) {
// 消费任务
System.out.println("消费:" + item);
}
// 删除已经执行的任务
jedis.zremrangeByScore(_KEY, lastSecond, nowSecond);
Thread.sleep(1000); // 每秒查询一次
}
}
}
② Уведомление о ключевом пространстве
Мы можем реализовать задачи синхронизации с помощью уведомлений о ключевом пространстве Redis. Его идея реализации состоит в том, чтобы установить время истечения для всех задач синхронизации. По истечении времени истечения мы можем понять, что задачи синхронизации необходимо выполнять, подписавшись на сообщения с истекшим сроком действия. выполнять запланированные задачи, мы можем это сделать.
По умолчанию Redis не включает уведомление о ключевом пространстве, нам нужно передатьconfig set notify-keyspace-events Ex
Команда открытия вручную, код запланированного задания после открытия следующий:
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPubSub;
import utils.JedisUtils;
public class TaskExample {
public static final String _TOPIC = "__keyevent@0__:expired"; // 订阅频道名称
public static void main(String[] args) {
Jedis jedis = JedisUtils.getJedis();
// 执行定时任务
doTask(jedis);
}
/**
* 订阅过期消息,执行定时任务
* @param jedis Redis 客户端
*/
public static void doTask(Jedis jedis) {
// 订阅过期消息
jedis.psubscribe(new JedisPubSub() {
@Override
public void onPMessage(String pattern, String channel, String message) {
// 接收到消息,执行定时任务
System.out.println("收到消息:" + message);
}
}, _TOPIC);
}
}
Для получения дополнительной информации о реализации задач на время нажмите "Самая полная сводка реализации отложенных задач в истории! прикрепленный код".
Обратите внимание на общественный номер»Сообщество китайского языка Java«Отправьте «Интервью», чтобы получить последние материалы обзора интервью, которые я собрал.