Redis系列四 - 在springboot中通过Lua脚本在redis中实现定时任务

有时我们需要在特定时间执行特定的任务,然而一般的定时任务又不满足我们的需求。如重推任务:我们向第三方发送话单,但是有可能推送失败,此时我们需要隔一段时间再重推。重推N次后,仍然失败,则不重推,标志无法推送程序需要在N秒后执行特定任务,但是任务的参数由当前决定。本文演示使用Redis,lua和spring boot实现如上的功能。
摘要由CSDN通过智能技术生成

1. 概述

有时我们需要在特定时间执行特定的任务,然而一般的定时任务又不满足我们的需求。如

  1. 重推任务:我们向第三方发送话单,但是有可能推送失败,此时我们需要隔一段时间再重推。重推N次后,仍然失败,则不重推,标志无法推送
  2. 程序需要在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各个参数值的说明

  1. key:timedTask_#{unique_keySuffix}
  2. member:#{id}
  3. score: 执行时间

B. Hash:保存每个定时任务的详细信息。相同类型的任务zset和hash的key的unique_keySuffix相同。从zset获取id后和hash的field进行匹配,从而获得fieldValue。fieldValue存储任务的详细信息,目前使用json字符串存储信息。

各个参数值的说明

  1. key:timedTaskContent_#{unique_keySuffix}
  2. field: #{id}
  3. 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(
  • 3
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值