Mastering Delayed Tasks in Java: From DB Polling to Redis and RabbitMQ
This article compares delayed tasks with scheduled tasks, explains their differences, and presents five practical Java solutions—including database polling, JDK DelayQueue, Netty time wheel, Redis implementations, and RabbitMQ—detailing each approach's design, code examples, advantages, and drawbacks.
Understanding Requirements
In development, delayed‑task scenarios frequently appear, such as automatically canceling an order after 30 minutes of non‑payment or sending an SMS 60 seconds after order creation. The term "delayed task" differs from a "scheduled task" in three key ways:
Scheduled tasks have a fixed trigger time; delayed tasks do not.
Scheduled tasks run periodically; delayed tasks execute once after an event.
Scheduled tasks often handle batch operations; delayed tasks usually handle a single operation.
Below we analyze solutions using order timeout checking as an example.
Solution 1: Database Polling
Idea
This approach suits small projects: a thread periodically scans the database, checks order timestamps, and updates or deletes expired orders.
Implementation
Early implementation used Quartz.
<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, easy to implement, supports clustering.
Cons
High memory consumption on the server.
Potential latency equal to the scan interval.
Heavy database load when scanning millions of orders.
Solution 2: JDK DelayQueue
Idea
Uses the built‑in DelayQueue, an unbounded blocking queue that only releases elements after their delay expires. Elements must implement the Delayed interface.
Workflow diagram:
Key methods: Poll(): Retrieves and removes expired elements, 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 (3‑second delay):
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(); }
}
}
}Output shows each order being processed after roughly 3 seconds.
Pros
High efficiency, low trigger latency.
Cons
Data lost on server restart.
Cluster expansion is cumbersome.
Potential OOM when many pending orders.
Higher code complexity.
Solution 3: Time Wheel Algorithm
Idea
The time wheel works like a clock: a rotating pointer (tick) moves at fixed intervals. Important parameters are ticksPerWheel, tickDuration, and timeUnit. Tasks are placed into slots based on their delay.
Illustration:
Implementation
Uses Netty's HashedWheelTimer. Add dependency:
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.24.Final</version>
</dependency>Demo code:
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;
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[] argv) {
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++;
}
}
}Output shows seconds counting up to 5, then the order deletion message.
Pros
High efficiency, lower latency than DelayQueue, and simpler code.
Cons
Data lost on server restart.
Cluster expansion is difficult.
Potential OOM with massive pending orders.
Solution 4: Redis Cache
Idea 1
Leverages Redis sorted set (zset) where the score stores the expiration timestamp and the member stores the order ID. Periodic scanning removes expired orders.
Key commands:
ZADD key score member ZRANGE key start stop [WITHSCORES] ZSCORE key member ZREM key memberImplementation snippet:
package com.rjzheng.delay4;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.Tuple;
import java.util.Calendar;
import java.util.Set;
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: create 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, "OID0000001" + i);
System.out.println(System.currentTimeMillis() + "ms: redis generated order OID0000001" + i);
}
}
// Consumer: poll and process 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();
}
}In high‑concurrency scenarios, multiple consumers may process the same order. The fix checks the return value of ZREM to ensure only one consumer proceeds.
Idea 2 (Keyspace Notifications)
Enables Redis to publish an event when a key expires. Add notify-keyspace-events Ex to redis.conf. A subscriber listens to the __keyevent@0__:expired channel.
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 + " 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 persist in Redis, reliable after restarts, easy cluster scaling, high time accuracy.
Cons: requires Redis maintenance and the Pub/Sub model is fire‑and‑forget, so events can be lost if the subscriber disconnects.
Solution 5: Message Queue (RabbitMQ)
Idea
RabbitMQ supports delayed queues via message TTL ( x-message-ttl) and dead‑letter exchange routing ( x-dead-letter-exchange, x-dead-letter-routing-key). When a message expires, it is dead‑lettered to a consumer queue, achieving delay.
Pros
High efficiency, leverages RabbitMQ's distributed nature for horizontal scaling, and supports message persistence for reliability.
Cons
Increases operational complexity and cost due to dependence on RabbitMQ infrastructure.
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 High-Performance Architecture
Sharing Java development articles and resources, including SSM architecture and the Spring ecosystem (Spring Boot, Spring Cloud, MyBatis, Dubbo, Docker), Zookeeper, Redis, architecture design, microservices, message queues, Git, etc.
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.
