Mastering Delayed Tasks: From Quartz to Redis and Beyond
This article compares delayed and scheduled tasks, explores five implementation strategies—including database polling, JDK DelayQueue, time‑wheel algorithm, Redis sorted sets, and RabbitMQ—provides code samples, analyzes pros and cons, and offers practical guidance for building reliable delayed‑task systems.
Introduction
In development you often encounter delayed‑task requirements, such as automatically cancelling an order after 30 minutes of no payment or sending an SMS 60 seconds after order creation.
Generate an order; if not paid within 30 minutes, cancel it automatically.
Generate an order; send an SMS to the user after 60 seconds.
We call this a delayed task . How does it differ from a scheduled task ? The differences are:
Scheduled tasks have a fixed trigger time; delayed tasks do not.
Scheduled tasks have an execution cycle; delayed tasks execute once after an event.
Scheduled tasks usually handle batch operations; delayed tasks generally handle a single operation.
Below we analyse the order‑timeout scenario and present several solutions.
Solution Analysis
(1) Database Polling
Idea
This approach is common in small projects: a thread periodically scans the database for overdue orders and updates or deletes them.
Implementation
The author originally used Quartz. Add the Maven dependency:
<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 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 every 3 seconds:
Scanning database for overdue orders...Pros and Cons
Pros: simple, easy to implement, supports clustering.
Cons: high memory consumption, latency (e.g., up to 3 minutes if scanning every 3 minutes), heavy DB load for large order volumes.
(2) JDK DelayQueue
Idea
Uses the JDK's unbounded blocking DelayQueue. Elements implement Delayed and become available only after their delay expires.
poll(): retrieves and removes the expired element, returns null if none. take(): blocks until an element expires, then returns it.
Implementation
Define OrderDelay implementing 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...");
}
}Test demo:
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 deleted after ~3 seconds:
00000001 order will be deleted...
After 3003 MilliSeconds
00000002 order will be deleted...
After 6006 MilliSeconds
...Pros and Cons
Pros: high efficiency, low trigger latency.
Cons: data lost on server restart, difficult cluster scaling, possible OOM for massive pending orders, higher code complexity.
(3) Time‑Wheel Algorithm
Idea
The algorithm works like a clock: a pointer rotates over slots (ticks). Each tick represents a fixed duration. Tasks are placed into slots based on their execution time.
Parameters: ticksPerWheel (slots per wheel), tickDuration (duration per tick), and timeUnit. For example, with 60 ticks, 1‑second duration, the wheel mimics a real‑second hand.
Implementation
Add Netty dependency:
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.24.Final</version>
</dependency>Test code using HashedWheelTimer:
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[] args) throws Exception {
MyTimerTask timerTask = new MyTimerTask();
Timer timer = new HashedWheelTimer();
timer.newTimeout(timerTask, 5, TimeUnit.SECONDS);
int i = 1;
while (timerTask.flag) {
Thread.sleep(1000);
System.out.println(i + " seconds passed");
i++;
}
}
}Output:
1 seconds passed
2 seconds passed
3 seconds passed
4 seconds passed
5 seconds passed
Deleting order from DB...
6 seconds passedPros and Cons
Pros: high efficiency, lower latency than DelayQueue, simpler code.
Cons: data lost on restart, difficult cluster scaling, OOM risk with massive tasks, higher code complexity.
(4) Redis Cache (Sorted Set)
Idea 1
Use Redis zset where the score is the expiration timestamp and the member is the order ID. Periodically scan the first element to check for timeout. ZADD key score member – add element. ZRANGE key start stop [WITHSCORES] – query ordered elements. ZSCORE key member – get score. ZREM key member – remove element.
Demo code (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(); }
// Producer – generate 5 orders with 3‑second delay
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, "OID000000" + i);
System.out.println(System.currentTimeMillis() + "ms: Redis generated order OID000000" + i);
}
}
// Consumer – poll and consume expired orders
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: Redis consumed order " + orderId);
}
}
}
}
public static void main(String[] args) {
AppTest app = new AppTest();
app.productionDelayMessage();
app.consumerDelayMessage();
}
}Output shows each order consumed after ~3 seconds.
1525086085261ms:redis generated order OID00000010
...
1525086088000ms:redis consumed order OID00000010
...Concurrency issue: multiple consumers may consume the same order. The fix is to check the return value of ZREM before processing.
Idea 2 – Keyspace Notifications
Configure Redis to publish expiration events: notify-keyspace-events Ex 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 + " order created");
}
}
static class RedisSub extends JedisPubSub {
@Override
public void onMessage(String channel, String message) {
System.out.println(System.currentTimeMillis() + "ms:" + message + " order cancelled");
}
}
}Sample output demonstrates order cancellation exactly after 3 seconds.
1525096202813ms:OID0000000 order created
...
1525096205819ms:OID0000000 order cancelled
...Pros and Cons
Pros: messages stored in Redis, reliable after restarts, easy cluster expansion, high timing accuracy.
Cons: requires Redis maintenance.
(5) Message Queue (RabbitMQ)
RabbitMQ supports delayed queues via x-message-ttl (message TTL) and dead‑letter exchange routing. This enables reliable delayed processing with persistence and horizontal scaling.
Pros and Cons
Pros: high efficiency, distributed, persistent messages increase reliability.
Cons: added operational complexity and cost due to RabbitMQ management.
Conclusion
The article summarizes the most common delayed‑task implementations in modern internet systems, providing code examples, performance trade‑offs, and practical tips to help engineers choose the right solution for their scenarios.
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.
Programmer DD
A tinkering programmer and author of "Spring Cloud Microservices in Action"
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.
