Backend Development 18 min read

Design and Implementation of Delayed Task Processing in Java: Database Polling, DelayQueue, Time Wheel, Redis, and RabbitMQ

The article explains the concept of delayed tasks versus scheduled tasks and presents five practical Java implementations—database polling with Quartz, JDK DelayQueue, Netty's HashedWheelTimer, Redis sorted sets and keyspace notifications, and RabbitMQ delayed queues—detailing their code, advantages, and drawbacks.

Architecture Digest
Architecture Digest
Architecture Digest
Design and Implementation of Delayed Task Processing in Java: Database Polling, DelayQueue, Time Wheel, Redis, and RabbitMQ

In development, delayed tasks such as automatically canceling unpaid orders after 30 minutes or sending an SMS after 60 seconds are common. Unlike scheduled tasks, delayed tasks have no fixed trigger time or period and usually handle a single operation.

Solution Analysis

(1) Database Polling

Suitable for small projects; a thread periodically scans the database for expired orders and updates or deletes them. Implementation example using Quartz with Maven dependency and a demo job class.

<dependency>
    <groupId>org.quartz-scheduler</groupId>
    <artifactId>quartz</artifactId>
    <version>2.2.2</version>
</dependency>
package com.rjzheng.delay1;

import org.quartz.*;
import org.quartz.impl.StdSchedulerFactory;

public class MyJob implements Job {
    public void execute(JobExecutionContext context) throws JobExecutionException {
        System.out.println("Scanning database for delayed tasks…");
    }
    public static void main(String[] args) throws Exception {
        JobDetail jobDetail = JobBuilder.newJob(MyJob.class)
                .withIdentity("job1", "group1")
                .build();
        Trigger trigger = TriggerBuilder.newTrigger()
                .withIdentity("trigger1", "group3")
                .withSchedule(SimpleScheduleBuilder.simpleSchedule()
                        .withIntervalInSeconds(3)
                        .repeatForever())
                .build();
        Scheduler scheduler = new StdSchedulerFactory().getScheduler();
        scheduler.scheduleJob(jobDetail, trigger);
        scheduler.start();
    }
}

Advantages: simple, cluster‑friendly. Disadvantages: high memory consumption, latency up to the scan interval, heavy DB load under large order volumes.

(2) JDK DelayQueue

Uses the built‑in DelayQueue which releases elements only after their delay expires. The element must implement Delayed .

package com.rjzheng.delay2;

import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;

public class OrderDelay implements Delayed {
    private String orderId;
    private long timeout;
    OrderDelay(String orderId, long timeout) {
        this.orderId = orderId;
        this.timeout = timeout + System.nanoTime();
    }
    public int compareTo(Delayed other) {
        if (other == this) return 0;
        OrderDelay t = (OrderDelay) other;
        long d = getDelay(TimeUnit.NANOSECONDS) - t.getDelay(TimeUnit.NANOSECONDS);
        return (d == 0) ? 0 : ((d < 0) ? -1 : 1);
    }
    public long getDelay(TimeUnit unit) {
        return unit.convert(timeout - System.nanoTime(), TimeUnit.NANOSECONDS);
    }
    void print() {
        System.out.println(orderId + " order will be deleted…");
    }
}
package com.rjzheng.delay2;

import java.util.*;
import java.util.concurrent.*;

public class DelayQueueDemo {
    public static void main(String[] args) {
        List
list = new ArrayList<>();
        list.add("00000001");
        list.add("00000002");
        list.add("00000003");
        list.add("00000004");
        list.add("00000005");
        DelayQueue
queue = new DelayQueue<>();
        long start = System.currentTimeMillis();
        for (int i = 0; i < 5; i++) {
            queue.put(new OrderDelay(list.get(i), TimeUnit.NANOSECONDS.convert(3, TimeUnit.SECONDS)));
            try {
                queue.take().print();
                System.out.println("After " + (System.currentTimeMillis() - start) + " Milliseconds");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

Advantages: high efficiency, low trigger latency. Disadvantages: data lost on server restart, difficult cluster scaling, possible OOM with massive pending orders, higher code complexity.

(3) Time Wheel (HashedWheelTimer)

Imitates a clock; each tick moves a pointer. Netty’s HashedWheelTimer is used.

package com.rjzheng.delay3;

import io.netty.util.*;
import java.util.concurrent.TimeUnit;

public class HashedWheelTimerTest {
    static class MyTimerTask implements TimerTask {
        boolean flag = true;
        public void run(Timeout timeout) throws Exception {
            System.out.println("Deleting order from DB…");
            flag = false;
        }
    }
    public static void main(String[] argv) {
        MyTimerTask timerTask = new MyTimerTask();
        Timer timer = new HashedWheelTimer();
        timer.newTimeout(timerTask, 5, TimeUnit.SECONDS);
        int i = 1;
        while (timerTask.flag) {
            try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); }
            System.out.println(i + " seconds passed");
            i++;
        }
    }
}

Advantages: efficient, lower latency than DelayQueue, simpler code. Disadvantages: same restart‑loss issue and scaling difficulty.

(4) Redis Zset

Orders are stored in a sorted set with the expiration timestamp as the score. A consumer scans the set, compares the current time, and removes expired orders.

package com.rjzheng.delay4;

import java.util.*;
import redis.clients.jedis.*;

public class AppTest {
    private static final String ADDR = "127.0.0.1";
    private static final int PORT = 6379;
    private static JedisPool jedisPool = new JedisPool(ADDR, PORT);
    public static Jedis getJedis() { return jedisPool.getResource(); }
    public void productionDelayMessage() {
        for (int i = 0; i < 5; i++) {
            Calendar cal = Calendar.getInstance();
            cal.add(Calendar.SECOND, 3);
            int score = (int) (cal.getTimeInMillis() / 1000);
            getJedis().zadd("OrderId", score, "OID0000001" + i);
            System.out.println(System.currentTimeMillis() + "ms: redis generated order OID0000001" + i);
        }
    }
    public void consumerDelayMessage() {
        while (true) {
            Set
items = getJedis().zrangeWithScores("OrderId", 0, 1);
            if (items == null || items.isEmpty()) {
                System.out.println("No pending tasks");
                try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); }
                continue;
            }
            int score = (int) ((Tuple) items.toArray()[0]).getScore();
            int now = (int) (Calendar.getInstance().getTimeInMillis() / 1000);
            if (now >= score) {
                String orderId = ((Tuple) items.toArray()[0]).getElement();
                Long removed = getJedis().zrem("OrderId", orderId);
                if (removed != null && removed > 0) {
                    System.out.println(System.currentTimeMillis() + "ms: redis consumed order " + orderId);
                }
            }
        }
    }
    public static void main(String[] args) {
        AppTest app = new AppTest();
        app.productionDelayMessage();
        app.consumerDelayMessage();
    }
}

To avoid multiple consumers processing the same order, the removal result is checked; only the consumer that successfully removes the element proceeds.

(5) Redis Keyspace Notifications

Enables callbacks when a key expires. After configuring notify-keyspace-events Ex , a subscriber receives expiration events and can handle order cancellation.

package com.rjzheng.delay5;

import redis.clients.jedis.*;

public class RedisTest {
    private static final String ADDR = "127.0.0.1";
    private static final int PORT = 6379;
    private static JedisPool jedis = new JedisPool(ADDR, PORT);
    private static RedisSub sub = new RedisSub();
    public static void init() {
        new Thread(() -> jedis.getResource().subscribe(sub, "__keyevent@0__:expired")).start();
    }
    public static void main(String[] args) throws InterruptedException {
        init();
        for (int i = 0; i < 10; i++) {
            String orderId = "OID000000" + i;
            jedis.getResource().setex(orderId, 3, orderId);
            System.out.println(System.currentTimeMillis() + "ms:" + orderId + " order created");
        }
    }
    static class RedisSub extends JedisPubSub {
        @Override
        public void onMessage(String channel, String message) {
            System.out.println(System.currentTimeMillis() + "ms:" + message + " order cancelled");
        }
    }
}

Pros: persistence in Redis, easy cluster expansion, accurate timing. Cons: requires Redis maintenance and Pub/Sub is fire‑and‑forget, so events may be lost if the client disconnects.

(6) RabbitMQ Delayed Queue

Leverages message TTL and dead‑letter exchange to implement delayed messages. Provides high efficiency, distributed capabilities, and message durability, but adds operational complexity and cost.

Overall, each solution balances trade‑offs between simplicity, performance, reliability, and scalability; the choice depends on the specific requirements of the system.

JavaRedisSchedulingRabbitMQdelayed tasksQuartzDelayQueue
Architecture Digest
Written by

Architecture Digest

Focusing on Java backend development, covering application architecture from top-tier internet companies (high availability, high performance, high stability), big data, machine learning, Java architecture, and other popular fields.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.