How to Implement Reliable Delayed Tasks in Java: From Quartz to Redis and RabbitMQ
This article compares several Java-based delayed‑task solutions—including database polling with Quartz, JDK DelayQueue, Netty’s HashedWheelTimer, Redis sorted‑set or key‑space notifications, and RabbitMQ delayed queues—detailing their implementations, advantages, drawbacks, and practical code examples for reliable order‑timeout handling.
# Introduction
In development, delayed tasks such as automatically canceling unpaid orders after 30 minutes or sending an SMS 60 seconds after order creation are common. A delayed task differs from a scheduled task in that it has no fixed trigger time, no execution cycle, and usually handles a single event.
# Solution Analysis
(1) Database Polling
Idea
This approach is suitable for small projects: a thread periodically scans the database for orders that have timed out and updates or deletes them.
Implementation
Using Quartz in a Maven project:
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz</artifactId>
<version>2.2.2</version>
</dependency>Demo job class:
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...");
}
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 “Scanning database…” every 3 seconds.
Pros: Simple, supports clustering. Cons: High memory consumption, latency up to the scan interval, heavy DB load for large order volumes.
(2) JDK DelayQueue
Idea
Utilizes the JDK’s unbounded blocking DelayQueue, which only releases elements after their delay expires. Elements must implement the Delayed interface.
Workflow diagram:
Key methods: poll() returns null if no expired element; take() blocks until an element expires.
Implementation
OrderDelay class:
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 + " will be deleted...");
}
}Demo:
package com.rjzheng.delay2;
import java.util.*;
import java.util.concurrent.*;
public class DelayQueueDemo {
public static void main(String[] args) {
List<String> list = Arrays.asList("00000001","00000002","00000003","00000004","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 deleted after roughly 3 seconds.
Pros: High efficiency, low trigger latency. Cons: Data lost on server restart, difficult cluster scaling, possible OOM with massive orders, higher code complexity.
(3) Time Wheel Algorithm
Idea
Imitates a clock where each tick advances the wheel; tasks are placed in slots based on their delay. Netty’s HashedWheelTimer provides this mechanism.
Diagram:
Implementation (pom dependency):
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.24.Final</version>
</dependency>Test code:
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) {
System.out.println("Deleting order from DB...");
flag = false;
}
}
public static void main(String[] args) {
MyTimerTask task = new MyTimerTask();
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++;
}
}
}Output demonstrates the task runs after 5 seconds.
Pros: Efficient, lower latency than DelayQueue, simpler code. Cons: Same restart and clustering drawbacks as DelayQueue.
(4) Redis Solutions
Idea 1 – Sorted Set
Store order IDs in a Redis zset with the timeout timestamp as the score. A consumer polls the set, compares the current time with the score, and removes the element if expired.
Key commands:
ZADD key score member
ZRANGE key start stop [WITHSCORES]
ZSCORE key member
ZREM key member
Implementation (producer & consumer):
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 JedisPool jedisPool = new JedisPool(ADDR, PORT);
public static Jedis getJedis() { return jedisPool.getResource(); }
// Produce 5 delayed orders
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);
}
}
// Consume expired orders
public void consumerDelayMessage() {
Jedis jedis = getJedis();
while (true) {
Set<Tuple> items = jedis.zrangeWithScores("OrderId", 0, 0);
if (items == null || items.isEmpty()) {
try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); }
continue;
}
Tuple first = items.iterator().next();
int score = (int) first.getScore();
int now = (int) (Calendar.getInstance().getTimeInMillis() / 1000);
if (now >= score) {
Long removed = jedis.zrem("OrderId", first.getElement());
if (removed != null && removed > 0) {
System.out.println(System.currentTimeMillis() + "ms: consumed order " + first.getElement());
}
}
}
}
public static void main(String[] args) {
AppTest app = new AppTest();
app.productionDelayMessage();
app.consumerDelayMessage();
}
}Workflow diagram:
Concurrent consumers may process the same order; fixing it by checking the return value of ZREM ensures only one consumer proceeds.
Idea 2 – Keyspace Notifications
Enable Redis key‑space events (e.g., notify-keyspace-events Ex) so that when a key expires, a message is published. A subscriber receives the event and can cancel the order.
Subscriber implementation:
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 pool = new JedisPool(ADDR, PORT);
private static 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");
}
}
}Output shows each order cancelled after 3 seconds.
Pros: Orders are persisted in Redis, reliable after restarts, easy cluster scaling, high time accuracy. Cons: Requires Redis maintenance.
(5) Message Queue (RabbitMQ)
RabbitMQ supports delayed queues via message TTL (x‑message‑ttl) and dead‑letter exchanges. When a message expires, it is routed to a dead‑letter queue, effectively implementing a delay.
Pros: High efficiency, built‑in clustering, persistent messages increase reliability. Cons: Additional operational overhead and complexity due to RabbitMQ dependency.
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.
Java Backend Technology
Focus on Java-related technologies: SSM, Spring ecosystem, microservices, MySQL, MyCat, clustering, distributed systems, middleware, Linux, networking, multithreading. Occasionally cover DevOps tools like Jenkins, Nexus, Docker, and ELK. Also share technical insights from time to time, committed to Java full-stack development!
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.
