Implementing Delayed Task Scheduling in Java: Quartz, DelayQueue, HashedWheelTimer, Redis, and RabbitMQ
The article explains the concept of delayed tasks versus scheduled tasks and provides five Java‑based solutions—Quartz polling, JDK DelayQueue, Netty HashedWheelTimer, Redis (zset and keyspace notifications), and RabbitMQ delayed queues—detailing their implementations, advantages, and drawbacks.
In many applications there is a need for delayed tasks, such as automatically cancelling an unpaid order after a certain period or sending a reminder SMS after a fixed delay. Unlike scheduled tasks, delayed tasks do not have a predetermined trigger time and usually execute only once after an event.
Solution 1: Database Polling with Quartz
This approach is suitable for small projects. A Quartz job periodically scans the database for orders that have exceeded their timeout and updates or deletes them.
Dependency to add:
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz</artifactId>
<version>2.2.2</version>
</dependency>Example job implementation (MyJob):
package com.rjzheng.delay1;
import org.quartz.JobBuilder;
import org.quartz.JobDetail;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.SchedulerFactory;
import org.quartz.SimpleScheduleBuilder;
import org.quartz.Trigger;
import org.quartz.TriggerBuilder;
import org.quartz.impl.StdSchedulerFactory;
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
public class MyJob implements Job {
public void execute(JobExecutionContext context) throws JobExecutionException {
System.out.println("Scanning database for timeout 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();
}
}Pros: simple, easy to set up, supports clustering. Cons: high memory consumption, latency depends on scan interval, heavy DB load under large data volumes.
Solution 2: JDK DelayQueue
DelayQueue is an unbounded blocking queue that only releases elements after their delay expires. Elements must implement the Delayed interface.
Implementation of a delayed order object:
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...");
}
}Demo that puts five orders with a 3‑second delay and takes them out:
package com.rjzheng.delay2;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.DelayQueue;
import java.util.concurrent.TimeUnit;
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();
}
}
}
}Pros: high efficiency, low trigger latency. Cons: data lost on JVM restart, difficult to scale in a cluster, possible OOM when many delayed orders exist, higher code complexity.
Solution 3: Netty HashedWheelTimer
HashedWheelTimer works like a clock wheel, providing low‑latency timing. Add Netty dependency:
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.24.Final</version>
</dependency>Sample timer task:
package com.rjzheng.delay3;
import io.netty.util.HashedWheelTimer;
import io.netty.util.Timeout;
import io.netty.util.Timer;
import io.netty.util.TimerTask;
import java.util.concurrent.TimeUnit;
public class HashedWheelTimerTest {
static class MyTimerTask implements TimerTask {
boolean flag = true;
public MyTimerTask(boolean flag) { this.flag = flag; }
public void run(Timeout timeout) throws Exception {
System.out.println("Deleting order from DB...");
this.flag = false;
}
}
public static void main(String[] args) {
MyTimerTask timerTask = new MyTimerTask(true);
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++;
}
}
}Pros: efficient, lower latency than DelayQueue, simpler code. Cons: same persistence issues as DelayQueue, cluster expansion is cumbersome, OOM risk under heavy load.
Solution 4: Redis‑Based Delayed Tasks
Two approaches are shown: using a sorted set (zset) and using key‑space notifications.
Approach A – ZSET stores the order ID as the member and the expiration timestamp as the score. A consumer periodically checks the smallest score and processes orders whose score is less than the current time.
package com.rjzheng.delay4;
import java.util.Calendar;
import java.util.Set;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.Tuple;
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() {
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) items.iterator().next().getScore();
int now = (int) (Calendar.getInstance().getTimeInMillis() / 1000);
if (now >= score) {
String orderId = items.iterator().next().getElement();
Long removed = jedis.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();
}
}Pros: high efficiency, low latency. Cons: data disappears on Redis restart, cluster coordination is non‑trivial, possible OOM when many pending orders exist.
Approach B – Keyspace Notifications enables Redis to publish an event when a key expires. A subscriber listens to the __keyevent@0__:expired channel and processes the order.
package com.rjzheng.delay5;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPubSub;
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 + " generated");
}
}
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 stored in Redis, allowing recovery after a crash; easy horizontal scaling; accurate timing. Cons: requires Redis maintenance.
Solution 5: RabbitMQ Delayed Queue
RabbitMQ can implement delayed messages by setting the x-message-ttl on a queue and configuring a dead‑letter exchange. When the TTL expires, the message is routed to the target queue for processing.
Pros: high performance, built‑in clustering, persistent messages increase reliability. Cons: adds operational complexity and cost due to RabbitMQ management.
Overall, the choice of implementation depends on factors such as persistence requirements, scalability, latency tolerance, and operational overhead.
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.
Top Architect
Top Architect focuses on sharing practical architecture knowledge, covering enterprise, system, website, large‑scale distributed, and high‑availability architectures, plus architecture adjustments using internet technologies. We welcome idea‑driven, sharing‑oriented architects to exchange and learn together.
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.
