Java HashedWheelTimer延时队列

参考文章:
https://www.javadoop.com/post/HashedWheelTimer

1 前言

在项目开发中,会有一些延时任务的需求,比如订单在30分钟之内未成功支付,则自动取消订单。此类需求可通过延迟队列实现,本文记录一下netty延时队列HashedWheelTimer的实现方式。

2 HashedWheelTimer介绍


(该图及以下介绍内容来自https://www.javadoop.com/post/HashedWheelTimer,想看HashedWheelTimer详细介绍和源码分析的也可以去看)

默认地,时钟每 100ms 滴答一下(tick),往前走一格,共 512 格,走完一圈以后继续下一圈。把它想象成生活中的钟表就可以了。

内部使用一个长度为 512 的数组存储,数组元素(bucket)的数据结构是链表,链表每个元素代表一个任务(Timeout 的实例)。

提交任务的线程,只要把任务往虚线上面的任务队列中存放即可返回。工作线程是单线程,一旦开启,不停地在时钟上绕圈圈。

仔细看下面的介绍:

  • 工作线程到达每个时间整点的时候,开始工作。在 HashedWheelTimer 中,时间都是相对时间,工作线程的启动时间,定义为时间的 0值。因为一次 tick 是 100ms(默认值),所以 100ms、200ms、300ms… 就是我说的这些整点。

  • 如上图,当时间到 200ms 的时候,发现任务队列有任务,取出所有的任务。

  • 按照任务指定的执行时间,将其分配到相应的 bucket 中。如上图中,任务2 和任务6指定的时间为 100ms~200ms这个区间,就被分配到第二个 bucket 中,形成链表,其他任务同理。

这里还有轮次的概念,不过不用着急,比如任务 6 指定的时间可能是 150ms + (512*100ms),它也会落在这个 bucket 中,但是它是下一个轮次才能被执行的。

  • 任务分配到 bucket 完成后,执行该次 tick 的真正的任务,也就是落在第二个 bucket 中的任务 2 和任务 6。

  • 假设执行这两个任务共消耗了 50ms,到达 250ms 的时间点,那么工作线程会休眠 50ms,等待进入到 300ms 这个整点

3 示例

以下为使用延迟队列HashedWheelTimer的示例:

3.1 pom依赖

        <dependency>
            <groupId>io.netty</groupId>
            <artifactId>netty-all</artifactId>
            <version>4.1.24.Final</version>
        </dependency>

3.2 创建任务

import io.netty.util.Timeout;
import io.netty.util.TimerTask;
import lombok.extern.slf4j.Slf4j;

/**
 * <pre>
 * 订单取消任务
 * </pre>
 *
 * @author loopy_y
 * @since 2022/6/14
 */
@Slf4j
public class OrderCancelTimerTask implements TimerTask {

    private String orderId;
    
    CancelOrderTimerTask(String orderId) {
        this.orderId = orderId;
    }

    @Override
    public void run(Timeout timeout) throws Exception {
        log.info("==== 订单【{}】取消操作执行中 ====", orderId);
        // TODO 以下省略具体业务实现
    }

}

3.3 创建延迟队列实例

import io.netty.util.HashedWheelTimer;
import io.netty.util.Timeout;
import io.netty.util.TimerTask;
import lombok.extern.slf4j.Slf4j;

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

/**
 * <pre>
 * 时间管理队列工具
 * </pre>
 *
 * @author yjj
 * @since 2022/6/14
 */
@Slf4j
public class OrderCancelTimer {

    private static HashedWheelTimer timer;
    /**
     * 延迟执行时间,可配置在配置文件中,这里为了方便测试设置为20s
     */
    private static long delay = 20L;
    /**
     * 存储Timeout对象,建立订单id与Timeout关系,用于通过订单id找到Timeout从队列中移除
     */
    private static Map<String, Timeout> timeoutMap = new HashMap<>();

    static {
        // 创建延迟队列实例,可以设置时间轮长度及刻度等,这里直接使用默认的
        timer = new HashedWheelTimer();
    }

    /**
     * 将任务添加进队列
     * @param timerTask 任务
     * @param orderId 订单id
     */
    public static void addNewTimeout(TimerTask timerTask, String orderId) {
        log.info("订单号【{}】准备添加进延迟队列", orderId);
        Timeout timeout = timer.newTimeout(timerTask, delay, TimeUnit.SECONDS);
        timeoutMap.put(orderId, timeout);
        log.info("订单号【{}】添加进延迟队列成功", orderId);
    }

    /**
     * 将任务从队列中移除
     * @param orderId 订单id (本示例中是通过订单id关联的Timeout对象,所以移除时需要根据此字段查询到Timeout对象)
     * @return 是/否成功移除
     */
    public static boolean delTimeout(String orderId) {
        log.info("订单号【{}】准备从延迟队列中移除", orderId);
        Timeout timeout = timeoutMap.get(orderId);
        boolean cancel = timeout.cancel();
        log.info("订单号【{}】从延迟队列中移除结果为:【{}】", orderId, cancel);
        if (cancel) {
            // 任务从队列中移除成功后,移除订单id和Timeout的关联关系
            timeoutMap.remove(orderId);
        }
        return cancel;
    }
}

3.4 编写测试Controller测试

import com.sinosoft.springbootplus.common.api.ApiResult;
import io.swagger.annotations.Api;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

/**
 * <pre>
 * 延迟队列测试 API
 * </pre>
 *
 * @author loopy_y
 * @since 2022/6/14
 */
@Slf4j
@RestController
@RequestMapping("/timer")
@Api(tags = "延迟队列API")
public class TimerTaskController {

    @GetMapping("/add")
    public ApiResult<Boolean> add(@RequestParam("orderId") String orderId) {
        OrderCancelTimer.addNewTimeout(new OrderCancelTimerTask(orderId), orderId);
        return ApiResult.ok();
    }

    @GetMapping("/delete")
    public ApiResult<Boolean> delete(@RequestParam("orderId") String orderId) {
        boolean delStatus = OrderCancelTimer.delTimeout(orderId);
        return ApiResult.ok(delStatus);
    }

}

3.5 测试结果

测试添加订单111、222、333、444,并把333订单从队列中移除,执行结果如下:

2022-06-14 17:54:59.357 [http-nio-8082-exec-18] INFO  [com.springbootplus.netty.HashedWheelTimerUtil:] [,] - 订单号【111】准备添加进延迟队列
2022-06-14 17:54:59.357 [http-nio-8082-exec-18] INFO  [com.springbootplus.netty.HashedWheelTimerUtil:] [,] - 订单号【111】添加进延迟队列成功
2022-06-14 17:55:01.115 [http-nio-8082-exec-19] INFO  [com.springbootplus.netty.HashedWheelTimerUtil:] [,] - 订单号【222】准备添加进延迟队列
2022-06-14 17:55:01.115 [http-nio-8082-exec-19] INFO  [com.springbootplus.netty.HashedWheelTimerUtil:] [,] - 订单号【222】添加进延迟队列成功
2022-06-14 17:55:03.050 [http-nio-8082-exec-20] INFO  [com.springbootplus.netty.HashedWheelTimerUtil:] [,] - 订单号【333】准备添加进延迟队列
2022-06-14 17:55:03.050 [http-nio-8082-exec-20] INFO  [com.springbootplus.netty.HashedWheelTimerUtil:] [,] - 订单号【333】添加进延迟队列成功
2022-06-14 17:55:05.349 [http-nio-8082-exec-21] INFO  [com.springbootplus.netty.HashedWheelTimerUtil:] [,] - 订单号【444】准备添加进延迟队列
2022-06-14 17:55:05.350 [http-nio-8082-exec-21] INFO  [com.springbootplus.netty.HashedWheelTimerUtil:] [,] - 订单号【444】添加进延迟队列成功
2022-06-14 17:55:07.536 [http-nio-8082-exec-22] INFO  [com.springbootplus.netty.HashedWheelTimerUtil:] [,] - 订单号【333】准备从延迟队列中移除
2022-06-14 17:55:07.536 [http-nio-8082-exec-22] INFO  [com.springbootplus.netty.HashedWheelTimerUtil:] [,] - 订单号【333】从延迟队列中移除结果为:【true】
2022-06-14 17:55:19.458 [pool-6-thread-1] INFO  [com.springbootplus.netty.CancelOrderTimerTask:] [,] - ==== 【111】订单取消操作执行中 ====
2022-06-14 17:55:21.163 [pool-6-thread-1] INFO  [com.springbootplus.netty.CancelOrderTimerTask:] [,] - ==== 【222】订单取消操作执行中 ====
2022-06-14 17:55:25.358 [pool-6-thread-1] INFO  [com.springbootplus.netty.CancelOrderTimerTask:] [,] - ==== 【444】订单取消操作执行中 ====

通过结果日志可以看出,订单号111、222、444成功添加进延迟队列中,并在20s后成功执行了超时后的业务操作。订单号333从队列中移除后,在20s后没有执行超时后的业务操作。测试结果完全符合预期。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Java中可以使用Redis实现延时队列。Redis是一个基于内存的键值对存储数据库,也被称为数据结构服务器,它支持多种数据结构,包括列表、哈希表、集合等。 要实现延时队列,可以使用Redis的有序集合(Sorted Set)数据结构。有序集合中的每个元素都有一个分数(score),根据分数的大小进行排序。我们可以将消息的到期时间作为分数,将消息体作为有序集合的成员。 以下是一个使用Java和Jedis客户端库来实现Redis延时队列的示例代码: ```java import redis.clients.jedis.Jedis; public class RedisDelayQueue { private static final String QUEUE_KEY = "delay_queue"; public void push(String message, long delay) { Jedis jedis = new Jedis("localhost"); jedis.zadd(QUEUE_KEY, System.currentTimeMillis() + delay, message); jedis.close(); } public void consume() { Jedis jedis = new Jedis("localhost"); while (true) { long currentTime = System.currentTimeMillis(); // 获取到期的消息 Set<String> messages = jedis.zrangeByScore(QUEUE_KEY, 0, currentTime); if (!messages.isEmpty()) { for (String message : messages) { // 处理消息 System.out.println("Consume message: " + message); // 从延时队列中移除已消费的消息 jedis.zrem(QUEUE_KEY, message); } } try { // 等待一段时间后再次检查是否有到期的消息 Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } jedis.close(); } public static void main(String[] args) { RedisDelayQueue redisDelayQueue = new RedisDelayQueue(); redisDelayQueue.push("message1", 5000); // 延时5秒 redisDelayQueue.push("message2", 10000); // 延时10秒 redisDelayQueue.consume(); } } ``` 在上述示例中,`push` 方法用于将消息加入延时队列,`consume` 方法用于消费到期的消息。可以在 `main` 方法中调用 `push` 方法添加消息,并调用 `consume` 方法启动消费者。 请注意,示例代码中仅实现了基本的延时队列功能,实际应用中可能还需要处理消息的持久化、消息重试等情况。此外,为保证高可用性和可靠性,建议使用Redis的主从复制或集群模式来部署。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值