基于Redis实现分布式定时任务调度

news/2025/2/26 4:32:36

项目开发过程中,难免会有许多定时任务的需求进来。如果项目中还没有引入quarzt框架的情况下,我们通常会使用Spring的@Schedule(cron="* * * * *")注解

样例如下:

package com.slowcity.redis;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Scheduled;

public class SentMailTask {
    private static final Logger log = LoggerFactory.getLogger(SentMailTask.class);
   /**
    * 定时任务
    */
    @Scheduled(cron = "0 0/1 * * * ? ")
    public void closeOrderTaskV1() {
        log.info(".........schedule task start.........");
        
        sentMailToCustomer();
        
        log.info(".........schedule task end.........");
    }
     
    public void sentMailToCustomer() {
        log.info(".........sent mail to customer.........");
    }
}

 

这样实现自然是没有什么问题,对于单台机器部署,任务每一分钟执行一次。部署多台机器时,同一个任务会执行多次

在我们的项目当中,使用定时任务是避免不了的,我们在部署定时任务时,通常只部署一台机器,此时可用性又无法保证现实情况是独立的应用服务通常会部署在两台及以上机器的时候,假如有3台机器,则会出现同一时间3台机器都会触发的情况,结果就是会向客户发送三封一模一样的邮件,真让人头疼。如果使用quarzt,就不存在这个情况了。

这种并发的问题,简单点说是锁的问题,具体点是分布式锁的问题,所以在这段代码上加个分布式锁就可以了。分布式锁,首先想到的是redis,毕竟轮子都是现成的。

package com.slowcity.redis;

import java.util.Collections;
import redis.clients.jedis.Jedis;

public class RedisPool {
    private static final String LOCK_SUCCESS="OK";
    private static final String SET_IF_NOT_EXIST="NX";
    private static final String SET_WITH_EXPIRE_TIME="PX";
    private static final Long RELEASE_SUCCESS=1L;
    
    /**
     * 获取分布式锁
     * @param jedis
     * @param lockKey
     * @param requestID
     * @param expireTime
     * @return
     */
    public static boolean getDistributedLock(Jedis jedis,String lockKey,String requestId,int expireTime) {
        String result = jedis.set(lockKey,requestId,SET_IF_NOT_EXIST,SET_WITH_EXPIRE_TIME,expireTime);
        if(LOCK_SUCCESS.equals(result)) {
            return true;
        }
        return false;
        
    }
    /**
     * 释放分布式锁
     * @param jedis
     * @param lockKey
     * @param requestId
     * @return
     */
    public static boolean releaseDistributedLock(Jedis jedis,String lockKey,String requestId) {
        String script = "if redis.call('get',KEYS[1])== ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
        Object result = jedis.eval(script,Collections.singletonList(lockKey),Collections.singletonList(requestId));
        if(RELEASE_SUCCESS.equals(result)) {
            return true;
        }
        return false;
    }
}

改造一下定时任务,增加分布式锁

package com.slowcity.redis;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Scheduled;

import redis.clients.jedis.Jedis;

public class SentMailTask {
    private static final Logger log = LoggerFactory.getLogger(SentMailTask.class);
   /**
    * 定时任务
    */
    @Scheduled(cron = "0 0/1 * * * ? ")
    public void closeOrderTaskV1() {
        log.info(".........schedule task start.........");
        Jedis jedis = new Jedis("10.2.1.17",6379);
        boolean locked = RedisPool.getDistributedLock(jedis, "", "", 10*1000);
        if(locked) {
            sentMailToCustomer();
        }
        RedisPool.releaseDistributedLock(jedis, "", "");
        jedis.close();
        log.info(".........schedule task end.........");
    }
     
    public void sentMailToCustomer() {
        log.info(".........sent mail to customer.........");
    }
}

再执行定时任务,多台机器部署,只执行一次。

关于jedis对象的获取,一般都是springboot自动化配置的,所有会想到工厂方法。优化如下:

 

package com.slowcity.redis;

import java.lang.reflect.Field;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.jedis.JedisConnection;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.util.ReflectionUtils;
import redis.clients.jedis.Jedis;

public class SentMailTask {
    private static final Logger log = LoggerFactory.getLogger(SentMailTask.class);
   
    @Autowired
    private RedisConnectionFactory redisConectionFactory;
    
    /**
    * 定时任务
    */
    @Scheduled(cron = "0 0/1 * * * ? ")
    public void closeOrderTaskV1() {
        log.info(".........schedule task start.........");
        
        RedisConnection redisConnection = redisConectionFactory.getConnection();
        Field jedisField = ReflectionUtils.findField(JedisConnection.class, "jedis");
        Jedis jedis = (Jedis) ReflectionUtils.getField(jedisField, redisConnection);
       
        boolean locked = RedisPool.getDistributedLock(jedis, "lockKey", "requestId", 10*1000);
        if(locked) {
            sentMailToCustomer();
        }
        RedisPool.releaseDistributedLock(jedis, "", "");
        jedis.close();
        log.info(".........schedule task end.........");
    }
     
    public void sentMailToCustomer() {
        log.info(".........sent mail to customer.........");
    }
}

 

再也不用担心,应用服务多台机器部署,每台机器都触发的尴尬了。如果定时任务很多,最好的还是老老实实写个任务调度中心,一来方便管理,二来方便维护。

补充部分:

 一些关于lua脚本的解释

String script = "if redis.call('get',KEYS[1])== ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
Object result = jedis.eval(script,Collections.singletonList(lockKey),Collections.singletonList(requestId));        

如果一个请求更新缓存的时间比较长,甚至比锁的有效期还要长,导致在缓存更新的过程中,锁就失效了,此时另一个请求就会获取锁,但前一个请求在缓存更新完毕的时候,如果不加以判断就直接删除锁,就会出现误删除其它请求创建的锁的情况。

【end】

一点补充的话,写完这篇博客后来看其他博客,也有一种redis锁是关联主机ip的,思路上是可行的,不失一个方法点,主要描述如下:

每个定时任务都在Redis中设置一个Key-Value,Key为自定义的每个定时任务的名字(如task1:redis:lock),Value为服务器Ip,同时设置合适的过期时间(例如设置为5min)。

每个节点在执行时,都要进行以下操作:

  • 1.是否存在Key,若不存在,则设置Key-Value,Value为当前节点的IP
  • 2.若存在Key,则比较Value是否是当前Ip,若是则继续执行定时任务,若不是,则不往下执行。

 


http://www.niftyadmin.cn/n/4054430.html

相关文章

linux路由route

一、永久添加路由 重启network服务生效 支持用#注释 方法一 a、添加默认网关,即默认路由 两块网卡在配置文件ifcfg-ethX中不配置网关,在/etc/sysconfig/network中设置默认网关 vim /etc/sysconfig/network GATEWAY192.168.14.254 b、添加路由 创…

mybatis-plus开启sql日志打印

方法一: mybatis-plus:configuration:log-impl: org.apache.ibatis.logging.stdout.StdOutImpl #开启sql日志或者:mybatis-plus:configuration:log-impl: org.apache.ibatis.logging.nologging.NoLoggingImpl #关闭sql日志 方法二: loggin…

phonegap(cordova) 自己定义插件代码篇(三)----支付宝支付工具整合

建议读者,先阅读官方文档,知晓其支付流程之后再来使用此代码,比方客户须要做什么,服务端须要做什么(非常重要!非常重要!非常重要!),由于这几个篇幅都是纯代码…

mybatis实现自定义分页插件

一、环境搭建 创建一个maven工程&#xff0c;然后引入mybatis依赖和mysql依赖即可。 <dependency><groupId>org.mybatis</groupId><artifactId>mybatis</artifactId><version>3.0.4</version> </dependency> <dependency…

Objective-C 引用计数原理

http://www.cocoachina.com/ios/20160112/14933.html 引用计数如何存储 有些对象如果支持使用 TaggedPointer&#xff0c;苹果会直接将其指针值作为引用计数返回&#xff1b;如果当前设备是 64 位环境并且使用 Objective-C 2.0&#xff0c;那么“一些”对象会使用其 isa 指针的…

使用Iterator对List集合进行增加或者删除操作时报异常的分析

一、问题 使用Iterator在对List集合进行遍历集合时&#xff0c;如果只是遍历而不进行增加、删除操作时&#xff0c;可以正常运行吗&#xff0c;但是如果我们在使用迭代器对List集合进行插入或者删除时&#xff0c;就会出现Exception in thread "main" java.util.Con…

SpringBoot实现本地(网络)文件下载和zip压缩包批量下载

一、前置条件 demo中&#xff0c;将多个文件通过zip的形式进行下载&#xff0c;所以需要引入以下maven坐标&#xff1a; <dependency><groupId>org.apache.commons</groupId><artifactId>commons-compress</artifactId><version>1.20<…

SpringBoot整合Quartz实现分布式调度

一、摘要 springboot quartz mysql 实现持久化分布式调度集群环境任务调度测试 二、Quartz 集群架构 Quartz 是 Java 领域最著名的开源任务调度工具。 在上篇文章中&#xff0c;我们详细的介绍了 Quartz 的单体应用实践&#xff0c;如果只在单体环境中应用&#xff0c;Qua…