如何定时任务?
在工作中,难免会遇到一些需要的定时任务,比如订单的状态,需要实时的根据过期时间来更新订单的状态。
那么如何做定时任务?方法很多,如下:
- 使用
JAVA
自带的的Timer
来做定时任务。 - 使用
Spring
的Task
来做定时任务,添加@Scheduler
注解来,来做定时任务。 - 使用
Quartz
来做定时任务。 - 使用
mysql
来做定时任务。 - 使用
redis
来做定时任务。
JAVA-Timer
这个不多讲,来一个Demo,大家自己看哈。
import cn.hutool.core.date.DateUtil;
import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;
public class Main {
public static void main(String[] args) {
//建立定时器
Timer timer = new Timer();
//定义一个任务
PrintTimer timerTask = new PrintTimer();
//开始执行任务:延迟5秒执行,每3秒执行一次
timer.schedule(timerTask, 5000, 3000);
System.out.println("开始时间:"+DateUtil.format(new Date(), "yyyy-MM-dd HH:mm:ss"));
timer.notify();
}
/**
* 继承TimerTask类,重写run方法
*/
static class PrintTimer extends TimerTask {
@Override
public void run() {
System.out.println("任务执行:"+DateUtil.format(new Date(), "yyyy-MM-dd HH:mm:ss"));
}
}
}
执行结果
开始时间:2023-09-22 15:08:38
任务执行:2023-09-22 15:08:43
任务执行:2023-09-22 15:08:47
任务执行:2023-09-22 15:08:50
任务执行:2023-09-22 15:08:53
任务执行:2023-09-22 15:08:56
//不终止会一直执行
SpringTask-Scheduler
这个也挺简单的,应用场景还不错,就是只能在单机进行,集群情况下,每个机器的每个服务都有可能会有这么一个定时任务,特别消耗资源,尤其是在定时任务里打印日志,会导致日志特别大!代码如下:
/**
* 随便建立一个类,加上扫描注解@Component,使其能够生成Bean
*/
@Component
public class ReportScheduleTask {
@Autowired
private OrderService orderService;
/**
* 每5秒执行一次,订单状态的更新
*/
@Scheduled(cron = "*/5 * * * * ?")
private void updateOrderStatus() {
orderService.updateOrderStatus()
}
}
对于
cron
表达式,不讲,链接一篇,自己看:cron表达式
Quartz
主要操作就是:
- 导包,
- 增加配置文件,
- 增加
Configuration
生成Quartz
的SchedulerFactoryBean
、QuartzInitializerListener
、通过工厂生成Scheduler
。 - 自定义任务
Job
。即实现Job
,并实现其方法execute
,咱们业务主要就在execute`里写。 - 生成定时器
Trigger
- 生成调度器
Scheduler
具体内容,不多赘述,链接两篇自己看: SpringBoot整合Quartz、 Quartz 基本使用
Mysql-Event_scheduler
mysql
的事件方面,其实在开发中用的很少啊,估计DBA才会经常用吧。
- 1、首先打开事件调度器
第一种方法:直接修改数据库
-- 查看事件调度器是否打开
SHOW VARIABLES LIKE 'event_scheduler';
SELECT @@event_scheduler;
-- 开启事件调度器
SET GLOBAL event_scheduler = ON;
-- 关闭事件调度器
SET GLOBAL event_scheduler = OFF;
第二种方法:修改配置文件my.ini
,改完重启mysql
。
#在mysqld下增加配置
[mysqld]
# 事件调度器改为启动状态
event_scheduler = on
- 2、创建事件
-- 语法及其解释
CREATE [DEFINER] (可选,用于定义事件执行时检查权限的用户,不写mysql会给自动加)
EVENT [IF NOT EXISTS] (可选,判断事件存在)
event_name (**必选,指定事件名称)
ON SCHEDULE schedule (**必选,事件执行时间,间隔)
[ON COMPLETION [NOT] PRESERVE] (可选,是否循环执行,默认不循环)
[ENABLE | DISABLE | DISABLE ON SLAVE] (可选,指定事件属性,默认启用)
[COMMENT 'comment'] (可选,定义事件注释)
DO event_body; (**必选,事件启动执行的代码)
实例:
-- 创建事件event_updateOrderStatus
-- schedule定时:从2023-09-21 16:28:26开始执行任务,每5秒执行一次SQL语句
-- 事件循环属性:永久执行
-- 当前该事件的状态:启用
-- 执行的SQL语句:UPDATE order SET STATUS = 2 WHERE expired_date < now()
CREATE DEFINER = `root` @`localhost`
EVENT `event_updateOrderStatus`
ON SCHEDULE
EVERY 5 SECOND STARTS '2023-09-21 16:28:26'
ON COMPLETION PRESERVE
ENABLE
COMMENT '更新订单状态'
DO
UPDATE order SET STATUS = 2 WHERE expired_date < now()
另外:关于msyql开启事件后,如何查看事件是否执行
即查看sql执行日志的问题:需要修改配置,有两种方法:
- 第一种方法:修改配置文件
my.ini
,重启程序
#在mysqld下增加配置
[mysqld]
# 开启日志,并设置日志路径
general_log=on
general_log_file=D:\ProgramData\MySQL80\Data\general.log
一般设置完毕,就会在你的日志目录下生成一个general.log
文件
- 第二种方法:直接修改数据库
show VARIABLES like '%general_log%'
直接修改配置项,即可。
Redis-[keyevent@*:expired]
关于Redis的定时任务,其实咱们是使用了redis
的发布订阅模式。将redis
中key过期
作为一个事件发布出来,那监听这个事件的程序就收到这key过期
的消息。
- 首先,开启事件的发布
第一种方法:使用命令
使用redis-cli.exe或者是其他的链接工具,比如:RDM,链接到服务器上,执行命令如下:
127.0.0.1:6379> config set notify-keyspace-events Ex
OK
这种方式很简单,但是重启后,就需要再执行一次命令。
第二种方法:修改配置文件
############################# EVENT NOTIFICATION ##############################
# 省略很多英文注释。。。。。
# 开始事件发布。键过期通知
notify-keyspace-events Ex
注释内容很多,其实是对当前事件的解释,告诉redis
发的是什么样的事件。解释如下:
Redis可以通过Pub/Sub,来通知客户端key空间中发生的事件。
后面的字符无论是什么,其实都是来设置 notify-keyspace-events 的,其中 Ex 表示开启键事件通知里面的 key 过期事件。
注意:如果启用了keyspace事件通知,并且客户端对存储在数据库0中的键“foo”执行DEL操作,
那么reids将会发布两条消息(即,key事件变动通知,以及key空间变动通知):
PUBLISH __keyspace@0__:foo del
PUBLISH __keyevent@0__:del foo
更多配置项说明如下:
K:键空间通知,所有通知以 __keyspace@<db>__ 为前缀
E:键事件通知,所有通知以 __keyevent@<db>__ 为前缀
g:DEL、EXPIRE、RENAME 等类型无关的通用命令的通知
$:字符串命令的通知
l:列表命令的通知
s:集合命令的通知
h:哈希命令的通知
z:有序集合命令的通知
x:过期事件,每当有过期键被删除时发送
e:驱逐(evict)事件,每当有键因为 maxmemory 政策而被删除时发送
A:参数 g$lshzxe 的别名
以上配置项可以自由组合,例如我们订阅列表事件就是 El,但需要注意的是,如果 notify-keyspace-event 的值设置为空,则表示不开启任何通知,有值则表示开启通知。
注意:这个地方大小写敏感的!!!
对了,这个地方我踩了一个小坑,大家改配置的时候,配置notify-keyspace-event
前面不能有空格。。。我第一次修改,只去掉了#,没去掉空格,导致Redis,启动就闪退…
- 导包
注意当前项目是Sprigboot的
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>2.7.10</version>
</dependency>
- 增加
Redis
配置类
生成RedisTemplate
、RedisMessageListenerContainer
,设置ChannelTopic
@Configuration
@EnableCaching
public class CacheConfig extends CachingConfigurerSupport {
/**
* spring boot redis默认序列化方式
*
* @param redisConnectionFactory the redis connection factory
* @return RedisTemplate redis template
*/
@Bean
public RedisTemplate<String, Object> getRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
JdkSerializationRedisSerializer jdkSerializationRedisSerializer = new JdkSerializationRedisSerializer();
final RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
redisTemplate.setKeySerializer(RedisSerializer.string());
redisTemplate.setHashKeySerializer(RedisSerializer.string());
//关于value值的序列化方法更换,请谨慎!!!
//FastJson的序列化方法,因FastJson包漏洞太多,不推荐,且项目中已经剔除该包
//Jackson的序列化方法(Jackson2JsonRedisSerializer、GenericJackson2JsonRedisSerializer),会导致@Cacheable注解反序列化user时报错,致使用户无法登陆。
//StringRedisSerializer需要存入的值都是String。increment()方法无法使用
//同样的JdkSerializationRedisSerializer会将value解析成对象的字符串,请注意,他同样会导致increment()方法无法使用
redisTemplate.setValueSerializer(jdkSerializationRedisSerializer);
redisTemplate.setHashValueSerializer(jdkSerializationRedisSerializer);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
/**
* spring redis 默认生成key方式,包含::号
*
* @param prefix the prefix
* @param key the key
* @return string
*/
public String simpleKeyGenerator(String prefix, String key) {
return CacheKeyPrefix.simple().compute(prefix) + key;
}
/**
* 创建Redis消息监听器
*
* @param connectionFactory
* @return
*/
@Bean
@Primary
public RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
return container;
}
/**
* 监听指定的库
* 这个地方,我当初设置的是库是 1,我链接的库是 4
* 但是最后Listener还是能收到其他库key过期的消息,不知道为啥。。。
* 建议大家改成 * ,直接监听所有库哈。不过这样消息会变多哈。
*/
@Bean
public ChannelTopic expiredTopic() {
return new ChannelTopic("__keyevent@*__:expired");
}
}
- 增加监听,用于监听key的变化
/**
* 订单过期的定时处理
*
* @author nimige
*/
@Slf4j
@Component
@Transactional
public class ResdisExpirationListener extends KeyExpirationEventMessageListener {
/**
* 需要更新状态的订单ID及其失败次数
*/
public static final String REDIS_ORDER_EXPIRE_KEY_PREFIX = "order:expire";
/**
* 用于记录更新状态的订单ID及其失败次数
*/
public static final String REDIS_ORDER_UPDATE_FAIL_KEY_PREFIX = "order:update:fail";
/**
* 重试次数
*/
public static final Integer MISSIONS_RETRIED = 5;
@Autowired
OrderService orderService;
@Autowired
CacheConfig cacheConfig;
@Autowired
private RedisService redisService;
public ResdisExpirationListener(RedisMessageListenerContainer listenerContainer) {
super(listenerContainer);
}
@Override
public void onMessage(Message message, byte[] pattern) {
String messageKey = message.toString();
if (messageKey.contains(REDIS_ORDER_EXPIRE_KEY_PREFIX)) {
Long orderId = Long.valueOf(messageKey.replace(CacheKeyPrefix.simple().compute(REDIS_ORDER_EXPIRE_KEY_PREFIX), ""));
try {
log.info("开始更新订单:{}", orderId);
Order order = orderService.selectOrderById(orderId);
Integer status = order == null ? null : order.getStatus();
if (status == null) {
log.error("当前订单状态不正确:订单ID:{}", orderId);
}
if (status.equals(WaitPay.getCode())) {
//代付款状态的订单才允许,修改为已过期哈
orderService.updateOrder(new Order().setId(orderId).setStatus(TimeOut.getCode()));
log.info("订单[{}]更新成功!", orderId);
}
} catch (Exception e) {
log.error("订单更新失败:", e);
String updateKey = cacheConfig.simpleKeyGenerator(REDIS_ORDER_UPDATE_FAIL_KEY_PREFIX, orderId.toString());
Object failCountObj = redisService.get(updateKey);
Integer failCount = failCountObj == null ? 1 : Integer.parseInt(failCountObj.toString());
log.error("更新订单失败:{},尝试更新(订单)次数:{}", orderId, failCount);
if (failCount < MISSIONS_RETRIED) {
//每5秒重试一次修改
redisService.set(messageKey, failCount, 5L);
failCount++;
//注意:因序列化方法的原因 increment()是无法使用的,因为存入的failCount是一个字符串
redisService.set(updateKey, failCount);
} else {
redisService.set(messageKey, failCount);
redisService.remove(updateKey);
log.error("订单[{}]数据处理次数达到上线", orderId);
}
}
}
}
}
- 最后是订单提交的时候,增加延时设置
public class OrderServiceImpl implements OrderService {
@Override
@Transactional
public Order orderSubmit(OrderSubmitVM orderSubmitVM, User user) {
// 省略无用代码。。。。。。
redisTemplate.opsForValue().set(cacheConfig.simpleKeyGenerator("order:expire", order.getId().toString()), 0, 30 * 60L, TimeUnit.SECONDS);
redisTemplate.opsForValue().set(cacheConfig.simpleKeyGenerator("order:update:fail", order.getId().toString()), 0);
}
}