基于Redis实现分布式定时任务调度
项目开发过程中,难免会有许多定时任务的需求进来。如果项目中还没有引入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,若是则继续执行定时任务,若不是,则不往下执行。
基于Redis实现分布式定时任务调度的更多相关文章
- 基于redis的分布式锁(转)
基于redis的分布式锁 1 介绍 这篇博文讲介绍如何一步步构建一个基于Redis的分布式锁.会从最原始的版本开始,然后根据问题进行调整,最后完成一个较为合理的分布式锁. 本篇文章会将分布式锁的实现分 ...
- 基于redis的分布式锁(不适合用于生产环境)
基于redis的分布式锁 1 介绍 这篇博文讲介绍如何一步步构建一个基于Redis的分布式锁.会从最原始的版本开始,然后根据问题进行调整,最后完成一个较为合理的分布式锁. 本篇文章会将分布式锁的实现分 ...
- redis系列:基于redis的分布式锁
一.介绍 这篇博文讲介绍如何一步步构建一个基于Redis的分布式锁.会从最原始的版本开始,然后根据问题进行调整,最后完成一个较为合理的分布式锁. 本篇文章会将分布式锁的实现分为两部分,一个是单机环境, ...
- 分布式定时任务调度系统技术解决方案(xxl-job、Elastic-job、Saturn)
1.业务场景 保险人管系统每月工资结算,平安有150万代理人,如何快速的进行工资结算(数据运算型) 保险短信开门红/电商双十一 1000w+短信发送(短时汇聚型) 工作中业务场景非常多,所涉及到的场景 ...
- 基于Redis的分布式锁设计
前言 基于Redis的分布式锁实现,原理很简单嘛:检测一下Key是否存在,不存在则Set Key,加锁成功,存在则加锁失败.对吗?这么简单吗? 如果你真这么想,那么你真的需要好好听我讲一下了.接下来, ...
- 基于redis 实现分布式锁的方案
在电商项目中,经常有秒杀这样的活动促销,在并发访问下,很容易出现上述问题.如果在库存操作上,加锁就可以避免库存卖超的问题.分布式锁使分布式系统之间同步访问共享资源的一种方式 基于redis实现分布式锁 ...
- 基于redis的分布式锁
<?php /** * 基于redis的分布式锁 * * 参考开源代码: * http://nleach.com/post/31299575840/redis-mutex-in-php * * ...
- 基于Redis的分布式锁真的安全吗?
说明: 我前段时间写了一篇用consul实现分布式锁,感觉理解的也不是很好,直到我看到了这2篇写分布式锁的讨论,真的是很佩服作者严谨的态度, 把这种分布式锁研究的这么透彻,作者这种技术态度真的值得我好 ...
- 基于 Redis 的分布式锁
前言 分布式锁在分布式应用中应用广泛,想要搞懂一个新事物首先得了解它的由来,这样才能更加的理解甚至可以举一反三. 首先谈到分布式锁自然也就联想到分布式应用. 在我们将应用拆分为分布式应用之前的单机系统 ...
随机推荐
- 带你认识MySQL sys schema
前言: MySQL 5.7中引入了一个新的sys schema,sys是一个MySQL自带的系统库,在安装MySQL 5.7以后的版本,使用mysqld进行初始化时,会自动创建sys库. sys库里 ...
- 【Easyexcel】java导入导出超大数据量的xlsx文件 解决方法
解决方法: 使用easyexcel解决超大数据量的导入导出xlsx文件 easyexcel最大支持行数 1048576. 官网地址: https://alibaba-easyexcel.github. ...
- Jar 打包与执行
Java学习笔记之一,用于个人记录.整理自<Head First Java>. 假设有如下目录结构: 程序入口在 Jukebox8.java.这个代码文件开头是有如下这样的包声明语句的: ...
- 在Linux系统中运行并简单的测试RabbitMq容器
以前使用的是Windows下面的RabbitMq,需要先安装 Erlang的语言环境等,这次直接在Linux中的Docker容器来测试一下 1:docker配置RabbitMq的指令 docker r ...
- Vs2017发布可在线更新的Winform程序
如题,此处引用“南秦岭”的博文<使用ClickOnce发布Windows应用程序>,对作者表示感谢! 补充说明: “发布文件夹”是指你电脑上的本地文件夹:“安装文件夹”是指你提供给用户的u ...
- pymysql 的简单使用
一.环境 Windows 7 x64 python 3.7.1 pymysql 0.9.3 mysql5.6.43 二.pymysql的简单使用 1.准备数据库demo_temp c ...
- JS运算符类型
一.运算符类型 1.算术运算符: 用于各类数值运算,包括加(+).减(-).乘(*).除(/).求余(或称模运算,%).自增(++).自减(--)共七种. 2.关系运算符: 用于比较运算.包括大于(& ...
- .Net网站防盗链
一.简单写法,在Global.asax下的这个方法中添加如下信息 /// <summary> /// 管道当中的第一个事件 /// </summary> /// <par ...
- 关于git回退版本的一点心得
我由于开发中不小心在master分支上开发,忘记了切换分支,最后我直接在master分支上提交,push,在开发分支上merge了master分支. 然后,同事告诉我他的代码要准备上线了,然而我的代码 ...
- tomcat特殊字符处理问题解决方案
tomcat特殊字符处理问题解决方案 直接加上如下代码,本质是通过反射加上过滤字符 @Configuration public class TomcatConfig { @Bean public Co ...