Mastering Delayed Tasks: From Database Polling to Redis and RabbitMQ

This article explains the concept of delayed tasks in payment systems, distinguishes them from scheduled tasks, and evaluates five practical implementation approaches—including database polling, Java DelayQueue, time‑wheel algorithms, Redis, and RabbitMQ—highlighting each method's advantages and drawbacks.

macrozheng
macrozheng
macrozheng
Mastering Delayed Tasks: From Database Polling to Redis and RabbitMQ

Today we present a solid technical solution for handling delayed tasks, a crucial component in payment systems such as automatically cancelling unpaid orders.

Typical delayed‑task scenarios include:

Cancel an order if it remains unpaid for 30 minutes.

Send an SMS to the user 60 seconds after an order is created.

We call this kind of work delayed task .

The main differences between delayed tasks and scheduled (cron) tasks are:

Scheduled tasks have a fixed trigger time; delayed tasks do not.

Scheduled tasks run periodically; delayed tasks execute once after an event.

Scheduled tasks usually handle batch operations; delayed tasks typically handle a single operation.

Solution Analysis

1) Database Polling

Idea

This approach is common in small projects: a dedicated thread periodically scans the database for orders that have exceeded their timeout and updates or deletes them.

Implementation

<dependency>
    <groupId>org.quartz-scheduler</groupId>
    <artifactId>quartz</artifactId>
    <version>2.2.2</version>
</dependency>

Demo job class:

public class MyJob implements Job {
    public void execute(JobExecutionContext context) throws JobExecutionException {
        System.out.println("Scanning database for overdue orders…");
    }
    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();
    }
}

Running the code prints the message every 3 seconds:

Scanning database for overdue orders…

Pros : Simple, easy to implement, supports clustering.

Cons :

High memory consumption on the server.

Potential latency (e.g., a 3‑minute scan can delay detection by up to 3 minutes).

Scanning millions of orders frequently can heavily load the database.

2) JDK DelayQueue

Idea

Use Java's built‑in DelayQueue, an unbounded blocking queue that only releases elements after their delay expires. Elements must implement the Delayed interface.

Implementation

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) { /* omitted for brevity */ }
    public long getDelay(TimeUnit unit) {
        return unit.convert(timeout - System.nanoTime(), TimeUnit.NANOSECONDS);
    }
    public void print() {
        System.out.println(orderId + " order will be deleted…");
    }
}

Demo test:

public class DelayQueueDemo {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("00000001");
        // ... add more ids ...
        DelayQueue<OrderDelay> 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) {}
        }
    }
}

Output shows each order being processed after roughly 3 seconds.

Pros : High efficiency, low trigger latency.

Cons :

Data is lost on server restart; not fault‑tolerant.

Cluster expansion is cumbersome.

Large numbers of pending orders may cause OOM.

Higher code complexity.

3) Time‑Wheel Algorithm

Idea

The time wheel works like a clock: a pointer moves at a fixed frequency, each tick representing a time slice. Tasks are placed into slots based on their expiration time.

Key parameters:

ticksPerWheel – number of ticks per rotation.

tickDuration – duration of a single tick.

timeUnit – unit of time.

Implementation using Netty's HashedWheelTimer:

<dependency>
    <groupId>io.netty</groupId>
    <artifactId>netty-all</artifactId>
    <version>4.1.24.Final</version>
</dependency>
public class HashedWheelTimerTest {
    static class MyTimerTask implements TimerTask {
        boolean flag = true;
        public void run(Timeout timeout) throws Exception {
            System.out.println("Deleting order from database…");
            flag = false;
        }
    }
    public static void main(String[] args) throws Exception {
        MyTimerTask task = new MyTimerTask();
        Timer timer = new HashedWheelTimer();
        timer.newTimeout(task, 5, TimeUnit.SECONDS);
        int i = 1;
        while (task.flag) {
            Thread.sleep(1000);
            System.out.println(i + " seconds passed");
            i++;
        }
    }
}

Output demonstrates the task executing after the configured delay.

Pros : High efficiency, lower latency than DelayQueue, simpler code.

Cons :

Data disappears after a server restart.

Cluster scaling is non‑trivial.

Large pending‑task volumes can cause OOM.

4) Redis Cache

Idea 1 – Sorted Set (ZSET)

Store the order ID as the member and the expiration timestamp as the score. Periodically query the smallest score to determine if a task is due.

ZADD key score member
ZRANGE key start stop [WITHSCORES]
ZSCORE key member
ZREM key member

Demo producer/consumer code (simplified):

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 ts = (int) (cal.getTimeInMillis() / 1000);
            getJedis().zadd("OrderId", ts, "OID000000" + i);
            System.out.println(System.currentTimeMillis() + " ms: generated order OID000000" + i);
        }
    }
    public void consumerDelayMessage() {
        while (true) {
            Set<Tuple> items = getJedis().zrangeWithScores("OrderId", 0, 0);
            if (items.isEmpty()) { Thread.sleep(500); continue; }
            Tuple t = items.iterator().next();
            int score = (int) t.getScore();
            int now = (int) (System.currentTimeMillis() / 1000);
            if (now >= score) {
                String orderId = t.getElement();
                Long removed = getJedis().zrem("OrderId", orderId);
                if (removed != null && removed > 0) {
                    System.out.println(System.currentTimeMillis() + " ms: consumed order " + orderId);
                }
            }
        }
    }
}

Using a distributed lock or checking the return value of ZREM prevents multiple consumers from processing the same order.

Idea 2 – Keyspace Notifications

Configure Redis to publish an event when a key expires (e.g., notify-keyspace-events Ex) and subscribe to the __keyevent@0__:expired channel.

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

After three seconds the expired‑order messages are printed.

Pros of Redis approach:

Messages are stored in Redis, providing durability if the producer or consumer crashes.

Easy horizontal scaling.

High timing accuracy.

Cons :

Requires additional Redis maintenance.

5) Message Queue (RabbitMQ)

RabbitMQ can implement delayed queues using the x-message-ttl property for messages and the x-dead-letter-exchange / x-dead-letter-routing-key settings for the queue. When a message expires it is routed to a dead‑letter queue, effectively achieving a delay.

Advantages: high efficiency, built‑in clustering, persistent messages increase reliability.

Disadvantages: adds operational complexity and cost due to the need to manage RabbitMQ.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

SchedulingMessage Queue
macrozheng
Written by

macrozheng

Dedicated to Java tech sharing and dissecting top open-source projects. Topics include Spring Boot, Spring Cloud, Docker, Kubernetes and more. Author’s GitHub project “mall” has 50K+ stars.

0 followers
Reader feedback

How this landed with the community

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.