1. 概述
有时我们需要在特定时间执行特定的任务,然而一般的定时任务又不满足我们的需求。如
- 重推任务:我们向第三方发送话单,但是有可能推送失败,此时我们需要隔一段时间再重推。重推N次后,仍然失败,则不重推,标志无法推送
- 程序需要在N秒后执行特定任务,但是任务的参数由当前决定。
本文演示使用redis,lua和spring boot实现如上的功能。
2. redis+lua实现基本的定时任务主功能
2.1. ITimedTaskService
此接口定义服务的基本方法:添加,删除和获取需要执行的定时任务
public interface ITimedTaskService{
/**
* 添加需要定时执行的任务
* @param keySuffix
* @param executeTime 执行的时间
* @param value
*/
<T extends ITimedTaskModel> T add(String keySuffix, final Date executeTime,final T value);
/**
* 批量删除已经执行的定时任务
* @param keySuffix
* @param relationValues
*/
void bathDel(String keySuffix, final String... ids);
/**
* 获取当前需要执行的定时任务
* @param keySuffix
* @return
*/
<T extends ITimedTaskModel> List<T> getTimedTaskContent(String keySuffix, Class<T> cls);
}
2.2. TimedTaskService
定时任务的主服务类,ITimedTaskService的实现类
具体实现原理说明
1. 变量定义:
- unique_keySuffi:任务的定时任务可以被多种定时任务共用,为了区分不同定时任务,所以不同任务的key后缀不同。每个不同的定时任务,需要定义唯一的后缀,如”cdrs”,”repush”
- id = UUID; //将ZSet和Hash里相应记录关联起来的值
2. redis定义两个key来保存定时任务的信息,2个key通过id值进行关联
A. ZSet: 核心是保存所有的定时任务计划将要执行的时间和hash关联的id值。不同类型的定时任务unique_keySuffix不同。相同类型的定时任务存储在相同的key,不同的同类型的任务通过member值区分,score存储将要执行的时间。通过zset的对score的排序功能,可以获取已经达到执行时间点的任务
key各个参数值的说明
- key:timedTask_#{unique_keySuffix}
- member:#{id}
- score: 执行时间
B. Hash:保存每个定时任务的详细信息。相同类型的任务zset和hash的key的unique_keySuffix相同。从zset获取id后和hash的field进行匹配,从而获得fieldValue。fieldValue存储任务的详细信息,目前使用json字符串存储信息。
各个参数值的说明
- key:timedTaskContent_#{unique_keySuffix}
- field: #{id}
- fieldValue: 执行定时任务所需要的参数
3. 关键方法说明:
添加任务:
• 一个任务需要同时在zset和hash中添加一条记录,两条记录通过id值关联在一起
• 在ZSet和Hash里根据以上规则各自添加1条新的记录
获取需要执行的任务:
• ZSet使用score保存任务执行时间,先从ZSet里面获取所有score <= 当前时间 的记录,
• 逐个根据zset的member值从hash中获取field和zset的member相同的fieldValue值(member和fieldValue都是id值),fieldValue存储本次需要执行任务的详细内容
删除记录
• 根据传入id值,从ZSet和Hash删除记录
使用lua脚本:
由于同时操作两个key,为了需要保证事物性,需要使用脚本
详细的实现Lua脚本如下:
add.lua:添加任务
-- save
-- hash info
local hashKey = KEYS[1]
local hashField = KEYS[2]
local hashFieldValue = KEYS[3]
-- zset info
local zSetKey = KEYS[4]
local zSetScore = KEYS[5]
local zSetMember = KEYS[6]
-- save hash
local result_1 = redis.call('HSET', hashKey, hashField, hashFieldValue)
-- save zset
local result_2 = redis.call('ZADD', zSetKey, zSetScore, zSetMember)
return result_1 + result_2
querycontents.lua :获取需要执行的任务
-- querycontents
-- ZSET key
local zSetKey = KEYS[1]
local zSetMin = KEYS[2]
local zSetMax = KEYS[3]
-- hash
local hashKey = KEYS[4]
-- run ZRANGEBYSCORE : 获取所有已经到了需要执行的定时任务
local zSetValues = redis.call('ZRANGEBYSCORE', zSetKey, zSetMin, zSetMax)
local rtnContentTables = {}
for k, v in pairs(zSetValues) do
-- run HGET : 获取定时任务的内容值
local hashField = v
local hashValue = redis.call('HGET', hashKey, hashField)
table.insert(rtnContentTables,hashValue)
redis.log(redis.LOG_DEBUG,hashField)
end
return rtnContentTables
batchdel.lua: 删除记录
-- del key
local result = 0
-- 参数的传入的规律:4个一组
for k, v in pairs(KEYS) do
if(k % 4 == 1 ) then
-- hash
local hashKey = KEYS[k];
local hashField = KEYS[k+1]
-- zset
local zSetKey = KEYS[k+2]
local zSetMember = KEYS[k+3]
-- run del hash
local result_1 = redis.call('HDEL', hashKey, hashField)
-- run del zset
local result_2 = redis.call('ZREM', zSetKey, zSetMember)
result = result_1 + result_2
end
end
return result
TimedTaskService:具体实现
@Service
public class TimedTaskService implements ITimedTaskService{
private static final Logger logger = LoggerFactory.getLogger(TimedTaskService.class);
private final String TIMED_TASK_KEY_PREFIX = "timedTask_"; // 所有定时任务的前缀都是此值
private final String TIMED_TASK_KEY_CONTENT_PREFIX = "timedTaskContent_"; // 所有定时任务的具体内容的前缀
@Autowired
private StringRedisTemplate redisTemplate;
// 添加操作
private DefaultRedisScript<Long> addScript;
// 删除操作
private DefaultRedisScript<Long> batchDelScript;
// 查询
private DefaultRedisScript<List> querycontentsScript;
@PostConstruct
public void init() {
// Lock script
addScript = new DefaultRedisScript<Long>();
addScript.setScriptSource(
new ResourceScriptSource(new ClassPathResource("com/hry/spring/redis/timedtask/add.lua")));
addScript.setResultType(Long.class);
// unlock script
batchDelScript = new DefaultRedisScript<Long>();
batchDelScript.setScriptSource(
new ResourceScriptSource(