Implementation Strategies for Delayed Tasks in Java Applications
The article explains the concept of delayed tasks, distinguishes them from scheduled tasks, and evaluates five practical implementation approaches—including database polling, JDK DelayQueue, Netty time‑wheel, Redis sorted‑set and key‑space notifications, and RabbitMQ delayed queues—while providing code samples, performance pros and cons, and deployment considerations.
In many business scenarios an order may need to be cancelled or a reminder sent after a certain period, which is referred to as a delayed task . Unlike a scheduled (cron) task, a delayed task has no fixed trigger time and is executed only after a specific event occurs.
Differences between Delayed and Scheduled Tasks
Scheduled tasks have a predetermined trigger time; delayed tasks do not.
Scheduled tasks repeat periodically; delayed tasks run once after the triggering event.
Scheduled tasks usually handle batch operations; delayed tasks typically handle a single operation.
Solution Analysis
(1) Database Polling
Periodically scan the order table to find expired orders and update or delete them. Simple to implement for small projects but consumes memory and may cause latency.
Implementation
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz</artifactId>
<version>2.2.2</version>
</dependency>Demo job using Quartz:
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("要去数据库扫描啦。。。");
}
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();
}
}(2) JDK DelayQueue
Uses the built‑in DelayQueue which only releases elements after their delay expires. Elements must implement Delayed .
Implementation
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) { /* omitted for brevity */ }
public long getDelay(TimeUnit unit) {
return unit.convert(timeout - System.nanoTime(), TimeUnit.NANOSECONDS);
}
void print() { System.out.println(orderId + "编号的订单要删除啦。。。。"); }
}Demo consumer:
package com.rjzheng.delay2;
import java.util.*;
import java.util.concurrent.*;
public class DelayQueueDemo {
public static void main(String[] args) {
List
list = new ArrayList<>();
list.add("00000001");
// ... add more ids
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) { e.printStackTrace(); }
}
}
}(3) Time‑Wheel Algorithm (Netty HashedWheelTimer)
Imitates a clock where each tick moves a pointer; tasks are placed in slots based on their delay.
Implementation
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("要去数据库删除订单了。。。。");
flag = false;
}
}
public static void main(String[] args) {
MyTimerTask timerTask = new MyTimerTask();
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++;
}
}
}(4) Redis Sorted‑Set (ZSET)
Store orderId as member and expiration timestamp as score. A consumer periodically checks the smallest score and removes expired orders.
Implementation
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(); }
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, "OID0000001" + i);
System.out.println(System.currentTimeMillis() + "ms:redis生成了一个订单任务:订单ID为OID0000001" + i);
}
}
public void consumerDelayMessage() {
Jedis jedis = 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;
}
Tuple t = items.iterator().next();
int score = (int) t.getScore();
int now = (int) (Calendar.getInstance().getTimeInMillis() / 1000);
if (now >= score) {
String orderId = t.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();
}
}Improved Concurrency
By checking the return value of zrem only the thread that successfully removes the member processes the order, avoiding duplicate consumption.
(5) Redis Key‑Space Notifications
Configure notify-keyspace-events Ex and subscribe to the __keyevent@0__:expired channel; when a key expires the subscriber receives a callback.
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 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 {
@Override
public void onMessage(String channel, String message) {
System.out.println(System.currentTimeMillis() + "ms:" + message + "订单取消");
}
}
}(6) Message Queue (RabbitMQ) Delayed Queue
RabbitMQ supports per‑message TTL ( x-message-ttl ) and dead‑letter exchange routing ( x-dead-letter-exchange , x-dead-letter-routing-key ) to achieve delayed processing.
Pros & Cons
High efficiency, supports horizontal scaling, persistent messages increase reliability.
Requires RabbitMQ operation and adds system complexity.
Conclusion
The article summarizes the most common delayed‑task implementations in modern internet systems, comparing their advantages, drawbacks, and suitable scenarios, helping engineers choose an appropriate solution for their business needs.
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.