Implementing Delayed Task Scheduling in Java: Quartz, DelayQueue, Time Wheel, Redis, and RabbitMQ
This article compares delayed tasks with scheduled tasks and presents six practical Java solutions—database polling with Quartz, JDK DelayQueue, Netty's HashedWheelTimer, Redis ZSET, Redis key‑space notifications, and RabbitMQ delayed queues—detailing their implementations, advantages, and drawbacks for handling order‑timeout scenarios.
In many applications, delayed tasks such as automatically cancelling an unpaid order after 30 minutes or sending an SMS after 60 seconds are required, and they differ from scheduled tasks in that they have no fixed trigger time, no execution cycle, and usually involve a single operation.
Solution 1: Database Polling (Quartz)
The approach uses a Quartz job that periodically scans the database for overdue orders and updates or deletes them. The Maven dependency required is:
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz</artifactId>
<version>2.2.2</version>
</dependency>Example job implementation:
public class MyJob implements Job {
public void execute(JobExecutionContext context) throws JobExecutionException {
System.out.println("要去数据库扫描啦。。。");
}
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, cluster‑friendly. Cons: high memory consumption, latency up to the scan interval, heavy DB load under high order volume.
Solution 2: JDK DelayQueue
This uses the java.util.concurrent.DelayQueue where each element implements Delayed . The queue releases elements only after their delay expires.
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 + "编号的订单要删除啦。。。。");
}
} public class DelayQueueDemo {
public static void main(String[] args) {
List
list = new ArrayList<>();
list.add("00000001");
list.add("00000002");
list.add("00000003");
list.add("00000004");
list.add("00000005");
DelayQueue
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) {}
}
}
}Pros: high efficiency, low trigger latency. Cons: data loss on JVM restart, difficult cluster scaling, possible OOM under massive pending orders, higher code complexity.
Solution 3: Time Wheel (Netty HashedWheelTimer)
The time‑wheel algorithm is similar to a clock; Netty provides HashedWheelTimer for low‑cost delayed execution.
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.24.Final</version>
</dependency> 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("要去数据库删除订单了。。。。");
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 + "秒过去了");
i++;
}
}
}Pros: high efficiency, lower latency than DelayQueue , simpler code. Cons: same persistence issues as solution 2, cluster expansion is non‑trivial.
Solution 4: Redis Sorted Set (ZSET)
Orders are stored in a Redis ZSET where the score is the expiration timestamp. A consumer periodically checks the smallest score and removes expired orders.
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 ts = (int) (cal.getTimeInMillis() / 1000);
AppTest.getJedis().zadd("OrderId", ts, "OID000000" + i);
System.out.println(System.currentTimeMillis() + "ms:redis生成了一个订单任务:订单ID为" + "OID000000" + i);
}
}
public void consumerDelayMessage() {
Jedis jedis = AppTest.getJedis();
while (true) {
Set
items = jedis.zrangeWithScores("OrderId", 0, 1);
if (items == null || items.isEmpty()) {
System.out.println("当前没有等待的任务");
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消费了一个任务:消费的订单OrderId为" + orderId);
}
}
}
}
public static void main(String[] args) {
AppTest app = new AppTest();
app.productionDelayMessage();
app.consumerDelayMessage();
}
}Pros: reliable persistence, easy cluster scaling, accurate timing. Cons: requires Redis maintenance.
Solution 5: Redis Key‑Space Notifications
By enabling notify-keyspace-events Ex in redis.conf , the application can subscribe to expiration events and process order cancellations.
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 + "订单生成");
}
}
static class RedisSub extends JedisPubSub {
public void onMessage(String channel, String message) {
System.out.println(System.currentTimeMillis() + "ms:" + message + "订单取消");
}
}
}Pros: reliable (messages stored in Redis), easy cluster expansion, high timing accuracy. Cons: depends on Redis availability and configuration.
Solution 6: RabbitMQ Delayed Queue
RabbitMQ can implement delayed messages using the x-message-ttl property and dead‑letter exchanges. By configuring a queue with x-dead-letter-exchange and x-dead-letter-routing-key , messages become available after the TTL expires.
Pros: high efficiency, built‑in clustering, message persistence adds reliability. Cons: adds operational complexity and cost due to RabbitMQ management.
Overall, each method has trade‑offs between simplicity, scalability, persistence, and latency; the choice depends on the specific requirements of the order‑timeout use case.
Architect's Guide
Dedicated to sharing programmer-architect skills—Java backend, system, microservice, and distributed architectures—to help you become a senior architect.
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.