Redis做定时任务

4 篇文章 0 订阅
2 篇文章 0 订阅

如何定时任务?

在工作中,难免会遇到一些需要的定时任务,比如订单的状态,需要实时的根据过期时间来更新订单的状态。
那么如何做定时任务?方法很多,如下:

  • 使用JAVA自带的的Timer来做定时任务。
  • 使用SpringTask来做定时任务,添加@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生成QuartzSchedulerFactoryBean QuartzInitializerListener 、通过工厂生成Scheduler
  • 自定义任务Job。即实现Job,并实现其方法execute,咱们业务主要就在execute`里写。
  • 生成定时器Trigger
  • 生成调度器Scheduler

具体内容,不多赘述,链接两篇自己看: SpringBoot整合QuartzQuartz 基本使用

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的发布订阅模式。将rediskey过期作为一个事件发布出来,那监听这个事件的程序就收到这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配置类
    生成RedisTemplateRedisMessageListenerContainer ,设置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);
    }
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

杀戮苍生

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值