延时任务通知服务的设计及实现(五)-- Netty时间轮HashedWheelTimer

一、背景

前文我们讲述了如何使用redisson的RDelayedQueue以及JDK的DelayQueue实现延迟队列,本文将补充一种实现延迟队列的方式--Netty的时间轮HashedWheelTimer。

它和JDK的DelayQueue一样,都存在着以下问题:

  • 占jvm内存,数据量大的时候,可能会导致OOM
  • 机器重启,内存中的延迟队列丢失
  • 需要解决分布式部署的问题

针对第一个问题,我们的解决办法是:

  • 1、设置热数据的时间范围,拉取通知时间将近的任务放入JVM内存,而非全部任务。热数据才存入延迟队列(我们称之为热数据),这样大大减少jvm内存。未到时间线的任务,存储在数据库中,我们称之为冷数据。
  • 2、使用分布式定时任务,对多个节点进行轮询,每次拉取100个任务放入延迟队列。通过轮询,不至于让任务都存放在同一个jvm内存。

针对第二个问题,我们的解决办法是:

  • 在应用重启后,查询热数据,将之存入延迟队列

针对第三个问题,我们的应对方法是:

  • 1、冷数据转换为热数据,使用分布式的定时任务。也就是说,同一个时间中,同一个任务只会存入某一个jvm内存。
  • 2、每次把任务放入延迟队列的时候,需要发布广播消息,由其他节点订阅,从延迟队列中移除该任务。

最后一点,我们在把任务放入延迟队列前,必须先删除jvm内存的任务。这样,防止同一个任务被重复存放至同一个jvm中的延迟队列。

另外,我们需要在任务执行的时候,判断期望执行时间是否和延迟队列中的任务执行时间一致,如果不一致,任务则忽略执行。

二、Netty时间轮HashedWheelTimer

本文许多部分和上文JDK的延迟队列DelayQueue 的许多流程类似,所以下面拣选重点描述。

1、定义任务

  • NettyTimerJob.java
java 复制代码
import io.netty.util.Timeout;
import io.netty.util.TimerTask;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;

import java.util.Date;

/**
 * @author xxx
 */
@Slf4j
@Data
public class NettyTimerJob implements TimerTask {

    /**
     * 通知任务
     */
    private NotifyTaskService notifyTaskService;
    /**
     * 交易流水号
     */
    private String transNo;
    /**
     * 到期时间
     */
    private Date expireDate;

    public NettyTimerJob(String transNo, Date expireDate, NotifyTaskService notifyTaskService) {
        this.transNo = transNo;
        this.expireDate = expireDate;
        this.notifyTaskService = notifyTaskService;
    }

    @Override
    public void run(Timeout timeout) throws Exception {
        if (log.isInfoEnabled()) {
            log.info("开始执行延迟队列中的任务,transNo={},expireDate={}", transNo, expireDate);
        }
        // 异步执行你的操作(任务的执行)
        notifyTaskService.handleTask(transNo, expireDate);
    }
}

2、封装时间轮

删除时间轮中的任务,需要使用Map存储任务唯一标识对应的Timeout对象,调用timeout.cancel()取消任务。

  • CustomHashedWheelTimer.java
java 复制代码
import io.netty.util.HashedWheelTimer;
import io.netty.util.Timeout;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;

/**
 * @author xxx
 */
public class CustomHashedWheelTimer {
    private final HashedWheelTimer timer;
    private final Map<String, Timeout> map;

    public CustomHashedWheelTimer(HashedWheelTimer timer) {
        this.timer = timer;
        this.map = new ConcurrentHashMap<>();
    }

    public void put(String taskId, NettyTimerJob job, long delay, TimeUnit unit) {
        // 如果任务已存在,则删除旧任务,防止重复添加
        this.remove(taskId);

        Timeout timeout = timer.newTimeout(job, delay, unit);
        // 将Timeout对象存储到Map中
        map.put(taskId, timeout);
    }

    public boolean remove(String taskId) {
        // 从Map中移除对应的Timeout对象
        Timeout timeout = map.remove(taskId);
        if (timeout != null) {
            // 取消任务
            return timeout.cancel();
        }

        return false;
    }
}
  • NettyTimerSingleton.java
java 复制代码
import io.netty.util.HashedWheelTimer;
import io.netty.util.concurrent.DefaultThreadFactory;

import java.util.concurrent.TimeUnit;

/**
 * @author xxx
 */
public class NettyTimerSingleton {

    private static volatile CustomHashedWheelTimer hashedWheelTimer;

    private NettyTimerSingleton() {
    }

    public static CustomHashedWheelTimer getInstance() {
        if (hashedWheelTimer == null) {
            synchronized (DelayQueueSingleton.class) {
                if (hashedWheelTimer == null) {
                    hashedWheelTimer = new CustomHashedWheelTimer(new HashedWheelTimer(
                            new DefaultThreadFactory("netty-timer"),
                            100,
                            TimeUnit.MILLISECONDS,
                            512,
                            true));
                }
            }
        }
        return hashedWheelTimer;
    }
}

3、操作延迟队列

  • 保存任务至延迟队列(生产者)
java 复制代码
if (DateUtil.offsetMinute(new DateTime(), commonConfig.getHotDataTimeLine()).after(event.getNotifyDate())) {
    final long delay = DateUtil.between(event.getNotifyDate(), new DateTime(), DateUnit.SECOND);

    NettyTimerSingleton.getInstance().put(event.getTransNo(),
            new NettyTimerJob(event.getTransNo(), event.getNotifyDate(), notifyTaskService),
            delay,
            TimeUnit.SECONDS);
    success = true;
}
  • 删除延迟队列的任务
java 复制代码
final String transNo = event.getTransNo();

NettyTimerSingleton.getInstance().remove(transNo);

三、总结

Netty时间轮和JDK的DelayQueue的最大区别是,你不需一直轮询延迟队列中的任务是否到期。

它的任务执行是在io.netty.util.TimerTask的run()方法中,所以我们在封装任务的时候,需要把NotifyTaskService对象放入,以便在任务执行的时候调用。

和JDK的DelayQueue一样,你需要使用一个Map集合持久化延迟队列,因为每次将任务放入延迟队列前,你都必须先删除之前的任务。否则同一个任务会在延迟队列中保存多份,一来浪费jvm内存,二来会导致任务的重复执行。(虽然我们在任务执行的时候,会对任务进行兜底,期望执行时间与延迟队列中的任务时间进行对比)

最后说一下,我们使用的netty版本是 4.1.43.Final,具体的依赖关系见下:

xml 复制代码
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.2.4.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

spring-boot-starter-data-redis依赖lettuce,后者又依赖netty,所以无需额外引入netty。

xml 复制代码
    <dependency>
      <groupId>io.lettuce</groupId>
      <artifactId>lettuce-core</artifactId>
      <version>5.2.1.RELEASE</version>
      <scope>compile</scope>
    </dependency>

当然,你如果引入了redisson框架,也无需额外引入netty框架。

由此也见,netty框架的使用范围之广。。。

附:相关系列文章链接

延时任务通知服务的设计及实现(一)-- 设计方案

延时任务通知服务的设计及实现(二)-- redisson的延迟队列RDelayedQueue

延时任务通知服务的设计及实现(三)-- JDK的延迟队列DelayQueue

延时任务通知服务的设计及实现(四)-- webhook执行任务

延时任务通知服务的设计及实现(五)-- Netty时间轮HashedWheelTimer

相关推荐
此木|西贝11 分钟前
【设计模式】模板方法模式
java·设计模式·模板方法模式
wapicn9921 分钟前
手机归属地查询Api接口,数据准确可靠
java·python·智能手机·php
hycccccch1 小时前
Springcache+xxljob实现定时刷新缓存
java·后端·spring·缓存
爱的叹息1 小时前
SpringBoot集成Redis 灵活使用 TypedTuple 和 DefaultTypedTuple 实现 Redis ZSet 的复杂操作
spring boot·redis·bootstrap
wisdom_zhe1 小时前
Spring Boot 日志 配置 SLF4J 和 Logback
java·spring boot·logback
揣晓丹1 小时前
JAVA实战开源项目:校园失物招领系统(Vue+SpringBoot) 附源码
java·开发语言·vue.js·spring boot·开源
你的人类朋友1 小时前
MQTT协议是用来做什么的?此协议常用的概念有哪些?
javascript·后端·node.js
于过1 小时前
Spring注解编程模型
java·后端
北随琛烬入1 小时前
Spark(10)配置Hadoop集群-集群配置
java·hadoop·spark
顽疲1 小时前
从零用java实现 小红书 springboot vue uniapp (11)集成AI聊天机器人
java·vue.js·spring boot·ai