DelayQueue之持久化方案

上一篇文章中提到了我们在项目中运用DelayQueue解决了一些需要延迟执行的任务,但是最近我们在生产环境上遇到了一个问题。重启服务器后,那些未执行的延迟任务就消失不见了。于是如何将延迟任务持久化就提上了日程。

关于DelayQueue的具体实现方案,已经在上一篇文章DelayQueue之通用组件中提到过了。本文就不再复述了。

本期的主题主要是探讨如何将延迟任务进行持久化。

何为延迟任务的持久化?顾名思义,就是将这些延迟任务的执行必要的数据,存储到数据库或redis等。

那么为何要进行持久化呢?很简单,因为延迟任务的数据是放在内存里,那么自己需要我们自己写持久化的备案以达到高可用,否则服务器故障宕机或新发布版本而导致服务器重启的时候,那么那些未执行的延迟任务数据将彻底丢失,这显然是我们不愿意见到的。

我目前采用的方案如下:
1、在需要使用到DelayQueue的地方,调用saveDelayTask方法,需要的参数有延迟任务函数策略工厂类的路由tag,执行方法所需的json格式的参数messageBody,延迟多久执行以秒为单位的delayTime。
2、任务调度每15秒去执行getNotCompletedMessageList方法。

大多数情况下,会在预计执行的时间点准时去执行processTask方法,那么异常状况下,如果服务器重启,那么定时任务调度会在一定时间后找到那些没有如期执行的延迟任务,通过定时任务调度的方式依次执行各自任务的processTask方法。
异常状态下,延迟任务执行会比预期执行时间有一定的延后,我设计的方案是目前我们可以允许的范围,这个大家可以酌情设置备选方案延后的时间。

核心代码如下,其他代码很简单就不一一公布了。

public void saveDelayTask(String tag, String messageBody, Long delayTime) {
    DelayTaskMessage delayTaskMessage = new DelayTaskMessage();
    delayTaskMessage.setTag(tag);
    LocalDateTime now = LocalDateTime.now();
    delayTaskMessage.setCreateTime(now);
    delayTaskMessage.setUpdateTime(now);
    delayTaskMessage.setDelayTime(delayTime);
    delayTaskMessage.setExpectedTime(now.plusSeconds(delayTime));
    delayTaskMessage.setMessageBody(messageBody);
    delayTaskMessage.setStatus(KafkaMessageStatusEnum.NOT_COMPLETE.getCode());
    int res = delayTaskMessageMapper.insertDelayTaskMessage(delayTaskMessage);
    if (res <= 0) {
        log.error("ybBrokerApp|insertDelayTaskMessage error, res<=0");
        throw new RuntimeException("insertDelayTaskMessage error, res<=0");
    }
    TaskMessage taskMessage = new TaskMessage(delayTime * 1000, messageBody,
            function -> this.processTask(delayTaskMessage));
    DelayQueue<TaskMessage> queue = taskManager.getQueue();
    queue.offer(taskMessage);
}

首先来分析一下,用来保存延迟任务的saveDelayTask方法。

tag是指延迟任务的标记,用于指定对应的策略类。

messageBody主要用于存储执行延迟任务的一些必要的数据,以json方法存储。

delayTime是延迟时间,默认以s为单位,主要是便于使用。

这个方法的主要功能是首先保存还未执行的延迟任务,自动根据延迟时间计算该延迟任务的预期执行时间,以便于后续的补偿算法跟踪,然后运用DelayQueue的特性,将这个延迟任务提交给延迟队列执行。

public int processTask(DelayTaskMessage param) {
    DelayTaskMessage delayTaskMessage = delayTaskMessageMapper.getDelayTaskMessageById(param.getId());
    try {
        if (null != delayTaskMessage && !Objects.equals(delayTaskMessage.getStatus(), KafkaMessageStatusEnum.NOT_COMPLETE.getCode())) {
            log.info("processTask executed already");
            return 1;
        }
        if (null != delayTaskMessage) {
            DelayTaskExecuteProcessor processor = processorFactory.getExecuteProcessor(delayTaskMessage.getTag());
            if (processor != null) {
                processor.execute(delayTaskMessage);
            } else {
                throw new RuntimeException("no such processor,tag=" + delayTaskMessage.getTag());
            }
            delayTaskMessage.setStatus(KafkaMessageStatusEnum.COMPLETE.getCode());
            delayTaskMessage.setExecutionTime(LocalDateTime.now());
            try {
                delayTaskMessage.setIpAddress(InetAddress.getLocalHost().getHostAddress());
            } catch (UnknownHostException ex) {
                log.error("Address.getLocalHost error", ex);
            }

            int res = delayTaskMessageMapper.updateDelayTaskMessageStatus(delayTaskMessage);
            if (res <= 0) {
                log.error("updateDelayTaskMessageStatus error res<=0");
                throw new RuntimeException("updateDelayTaskMessageStatus error");
            }
            return 1;
        } else {
            log.error("ybBrokerApp processTask error, delayTaskMessage is null delayTaskMessageId=", param.getId());
            return 0;
        }
    } catch (Exception e) {
        log.error("ybBrokerApp processTask error , param = " + param.toString() + "|", e);
        if (null != delayTaskMessage) {
            delayTaskMessage.setStatus(KafkaMessageStatusEnum.FAIL.getCode());
            delayTaskMessage.setErrorStack(e.getMessage());
            try {
                delayTaskMessage.setIpAddress(InetAddress.getLocalHost().getHostAddress());
            } catch (UnknownHostException ex) {
                log.error("Address.getLocalHost error", ex);
            }
            delayTaskMessageMapper.updateDelayTaskMessageStatus(delayTaskMessage);
        }
        return 0;
    }
}

然后是核心的处理延迟任务的processTask方法。

1、根据id,在数据库寻找到对应需要执行的延迟任务的持久化数据。
2、如果这条持久化数据非空且状态不是未执行的状态,那么提示该任务已经被执行过,防止重复执行。这里的status,主要有三种状态,未执行,已执行成功和执行失败。
3、如果这条持久化数据非空,且是未执行的状态,那么找到tag对应的策略类执行对应的execute方法。
4、将执行方法的ip地址记录下来,便于后续分析,同时将这条持久化数据的状态改为已执行成功的状态。
5、如果执行失败,将ip地址记录下来,同时把数据的状态改为执行失败。

在第4步中为何要先执行方法,后改状态呢?我是这么想的,延迟任务的执行方法是外部写的,在组件设计的时候无法控制会不会出现异常,而修改持久化数据的状态的方法是可控的,所以从这个角度上,我觉得先执行方法再修改状态更合理些。

需要注意的是,这个方法没有考虑并发的情况,是因为我在补偿方案里又额外延迟了一段时间,出现并发的情况非常小,不是很有必要考虑这种情况。

public List<DelayTaskMessage> getNotCompletedMessageList(int total, int index) {
        LocalDateTime expectedTime = LocalDateTime.now().plusSeconds(15L);
        List<DelayTaskMessage> delayTaskMessageList = delayTaskMessageMapper.getNotCompletedMessageList(expectedTime,total, index);
        if (CollectionUtils.isEmpty(delayTaskMessageList)) {
            return Lists.newArrayList();
        }
        return delayTaskMessageList;
    }

最后是补偿方案的落实,我是在定时任务中去保证延迟任务一定会被执行至少一次的。

我的设计是每隔15s去遍历一下那些过了预期执行时间+15s依然未执行的的延迟任务。然后将这些列表中的延迟任务重新调用processTask方法。

如果最终是通过补偿方案执行的延迟任务会比预期执行时间还要晚执行15到30s。目前在我们的项目中,这个额外延迟是可以被接收的。大家还是要根据实际情况酌情修改这个额外延迟的时间。

以上是我针对DelayQueue设计的的持久化方案,如果大家有更好的意见,可以一起讨论哦。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,602评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,442评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,878评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,306评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,330评论 5 373
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,071评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,382评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,006评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,512评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,965评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,094评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,732评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,283评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,286评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,512评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,536评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,828评论 2 345

推荐阅读更多精彩内容