Implementing Delayed Tasks in Java: Database Polling, JDK DelayQueue, Time Wheel, Redis, and RabbitMQ
This article explains the concept of delayed tasks, compares them with scheduled tasks, and presents five practical implementation strategies—including database polling with Quartz, JDK DelayQueue, Netty's time‑wheel algorithm, Redis sorted‑set and keyspace notifications, and RabbitMQ delayed queues—complete with code samples, advantages, and drawbacks.
1. Introduction
In development, delayed tasks such as canceling unpaid orders after 30 minutes or sending an SMS after 60 seconds are common. This article defines delayed tasks, distinguishes them from scheduled tasks, and lists their main characteristics.
2. Database Polling
This approach uses a dedicated thread to periodically scan the database for overdue orders. An early implementation used Quartz; the Maven dependency is shown below, followed by 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 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();
}
}Advantages: simple, easy to implement, supports clustering. Disadvantages: high memory consumption, latency up to the scan interval, heavy DB load under large order volumes.
3. JDK DelayQueue
The JDK provides an unbounded blocking queue that only releases elements after their delay expires. Elements must implement the Delayed interface.
package com.rjzheng.delay2;
import java.util.concurrent.*;
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 o = (OrderDelay) other;
long diff = getDelay(TimeUnit.NANOSECONDS) - o.getDelay(TimeUnit.NANOSECONDS);
return diff == 0 ? 0 : (diff < 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<String> list = new ArrayList<>();
list.add("00000001");
list.add("00000002");
list.add("00000003");
list.add("00000004");
list.add("00000005");
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) { e.printStackTrace(); }
}
}
}Output shows each order being processed after a 3‑second delay. Pros: high efficiency, low trigger latency. Cons: data loss on server restart, difficult cluster scaling, possible OOM under massive order volume, higher code complexity.
4. Time Wheel Algorithm
The time wheel works like a clock: a pointer (tick) moves at a fixed frequency. Important parameters are ticksPerWheel, tickDuration, and timeUnit. Netty's HashedWheelTimer is used for implementation.
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.24.Final</version>
</dependency> package com.rjzheng.delay3;
import io.netty.util.*;
import java.util.concurrent.TimeUnit;
public class HashedWheelTimerTest {
static class MyTimerTask implements TimerTask {
boolean flag;
MyTimerTask(boolean flag) { this.flag = flag; }
public void run(Timeout timeout) throws Exception {
System.out.println("Deleting order from DB…");
flag = false;
}
}
public static void main(String[] args) {
MyTimerTask task = new MyTimerTask(true);
Timer timer = new HashedWheelTimer();
timer.newTimeout(task, 5, TimeUnit.SECONDS);
int i = 1;
while (task.flag) {
try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); }
System.out.println(i + " seconds passed");
i++;
}
}
}Pros: high efficiency, lower latency than DelayQueue, simpler code. Cons: same restart‑data‑loss issue, cluster expansion difficulty, possible OOM under heavy load.
5. Redis Cache
Two approaches are presented. The first uses a sorted set (zset) where the score stores the expiration timestamp; a consumer periodically checks the smallest score and removes expired orders.
package com.rjzheng.delay4;
import redis.clients.jedis.*;
import java.util.*;
public class AppTest {
private static final String ADDR = "127.0.0.1";
private static final int PORT = 6379;
private static final JedisPool pool = new JedisPool(ADDR, PORT);
public static Jedis getJedis() { return pool.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, "OID0000001" + i);
System.out.println(System.currentTimeMillis() + "ms: generated order OID0000001" + i);
}
}
public void consumerDelayMessage() {
Jedis jedis = getJedis();
while (true) {
Set<Tuple> items = jedis.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 = jedis.zrem("OrderId", orderId);
if (removed != null && removed > 0) {
System.out.println(System.currentTimeMillis() + "ms: consumed order " + orderId);
}
}
}
}
public static void main(String[] args) {
AppTest at = new AppTest();
at.productionDelayMessage();
at.consumerDelayMessage();
}
}The second approach leverages Redis key‑space notifications ("notify-keyspace-events Ex") to receive a callback when a key expires, using Pub/Sub. This method provides higher reliability but depends on Redis version ≥2.8 and suffers from the fire‑and‑forget nature of Pub/Sub.
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 final JedisPool pool = new JedisPool(ADDR, PORT);
private static final RedisSub sub = new RedisSub();
public static void init() {
new Thread(() -> pool.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;
pool.getResource().setex(orderId, 3, orderId);
System.out.println(System.currentTimeMillis() + "ms:" + orderId + " created");
}
}
static class RedisSub extends JedisPubSub {
@Override
public void onMessage(String channel, String message) {
System.out.println(System.currentTimeMillis() + "ms:" + message + " order cancelled");
}
}
}Pros: messages are persisted in Redis, easier cluster scaling, high timing accuracy. Cons: requires additional Redis maintenance.
6. Message Queue
RabbitMQ can implement delayed queues by setting x-message-ttl on messages or queues and configuring x-dead-letter-exchange and x-dead-letter-routing-key to re‑route expired messages. This provides efficient, horizontally scalable delayed processing with persistence, though it adds operational complexity.
Overall, the article compares five practical solutions for delayed task execution, discussing their code implementations, strengths, and weaknesses, helping backend engineers choose the most suitable approach for their systems.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Code Ape Tech Column
Former Ant Group P8 engineer, pure technologist, sharing full‑stack Java, job interview and career advice through a column. Site: java-family.cn
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
