Mastering Single-Node and Distributed Scheduled Tasks in Java
This article compares native JDK ScheduledExecutorService, Spring Task, Redis‑based approaches, and popular distributed scheduling frameworks such as Quartz, Elastic‑Job, LTS, and XXL‑Job, providing code examples, usage scenarios, advantages, and best‑practice recommendations for implementing reliable timed jobs in microservice environments.
Single-Node Scheduled Tasks
JDK Native
Since JDK 1.5, ScheduledExecutorService replaces TimerTask for scheduled tasks, offering better reliability.
public class SomeScheduledExecutorService {
public static void main(String[] args) {
// Create a thread pool with 10 threads
ScheduledExecutorService scheduledExecutorService =
Executors.newScheduledThreadPool(10);
// Execute task: start after 10 seconds, run every 30 seconds
scheduledExecutorService.scheduleAtFixedRate(() -> {
System.out.println("执行任务:" + new Date());
}, 10, 30, TimeUnit.SECONDS);
}
}Spring Task
Spring Framework provides built‑in scheduling with cron expressions. Beginners can use https://cron.qqe2.com/ to generate cron strings.
@Configuration
@EnableScheduling
public class SomeJob {
private static final Logger LOGGER = LoggerFactory.getLogger(SomeJob.class);
/**
* Execute every minute (e.g., 18:01:00, 18:02:00)
* second minute hour day month week year
*/
@Scheduled(cron = "0 0/1 * * * ? *")
public void someTask() {
// ...
}
}In microservice environments, single‑node scheduling becomes limited, so distributed scheduling is recommended.
Redis‑Based Implementation
Redis can store scheduled tasks in a ZSet, using the score field to hold the execution timestamp. A periodic scan checks for tasks whose score is less than the current time and executes them.
Using ZSet
Implementation code:
@Configuration
@EnableScheduling
public class RedisJob {
public static final String JOB_KEY = "redis.job.task";
private static final Logger LOGGER = LoggerFactory.getLogger(RedisJob.class);
@Autowired
private StringRedisTemplate stringRedisTemplate;
/** Add a task */
public void addTask(String task, Instant instant) {
stringRedisTemplate.opsForZSet().add(JOB_KEY, task, instant.getEpochSecond());
}
/** Consume tasks every minute */
@Scheduled(cron = "0 0/1 * * * ? *")
public void doDelayQueue() {
long nowSecond = Instant.now().getEpochSecond();
Set<String> strings = stringRedisTemplate.opsForZSet().range(JOB_KEY, 0, nowSecond);
for (String task : strings) {
LOGGER.info("执行任务:{}", task);
}
stringRedisTemplate.opsForZSet().remove(JOB_KEY, 0, nowSecond);
}
}Typical scenarios:
Cancel an unpaid order 15 minutes after creation.
Return unclaimed red packets after 24 hours.
Activate or deactivate a feature at a specific time.
Advantages:
Avoids MySQL queries by using Redis.
Resilient to node failures.
Keyspace Notification Method
Redis keyspace notifications can trigger tasks when a key expires. Enable it with config set notify-keyspace-events Ex.
Custom Listener
public class KeyExpiredListener extends KeyExpirationEventMessageListener {
public KeyExpiredListener(RedisMessageListenerContainer listenerContainer) {
super(listenerContainer);
}
@Override
public void onMessage(Message message, byte[] pattern) {
String channel = new String(message.getChannel(), StandardCharsets.UTF_8);
String key = new String(message.getBody(), StandardCharsets.UTF_8);
// TODO: handle expired key
}
}Register Listener
@Configuration
public class RedisExJob {
@Autowired
private RedisConnectionFactory redisConnectionFactory;
@Bean
public RedisMessageListenerContainer redisMessageListenerContainer() {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(redisConnectionFactory);
return container;
}
@Bean
public KeyExpiredListener keyExpiredListener() {
return new KeyExpiredListener(this.redisMessageListenerContainer());
}
}Spring listens to messages matching pattern __keyevent@*.
Redis‑based scheduling is suitable for delay‑task scenarios but requires idempotent handling.
Distributed Scheduled Tasks
Introducing Middleware Components
Running scheduled jobs as a separate service prevents duplicate execution and simplifies scaling.
Quartz
Depends on MySQL, supports multi‑node deployment via database lock competition. No built‑in UI.
Elastic‑Job‑Lite
Depends on Zookeeper, offers dynamic server registration, multiple job modes, failover, status collection, sharding, idempotency, fault tolerance, Spring namespace support, and a graphical UI.
LTS
Based on Zookeeper, provides cluster deployment, dynamic task addition, pause/resume, business logging, SPI extensions, failover, node monitoring, diverse result handling, FailStore, dynamic scaling, Spring friendliness, and a management UI.
XXL‑Job
Chinese open‑source solution using MySQL for lock competition, supports horizontal scaling, manual task management, elastic expansion, sharding broadcast, failover, real‑time logs, online code editing (GLUE), progress monitoring, task dependencies, encryption, email alerts, reporting, graceful shutdown, and internationalization.
Conclusion
In microservice architectures, using a dedicated scheduler such as XXL‑Job is recommended for reliable task management. Spring Task offers the simplest setup, while XXL‑Job requires more integration effort. Regardless of the approach, ensure tasks are not executed multiple times in a cluster, handle exceptions, avoid backlog, and run at the intended time.
Middleware decouples services but adds complexity.
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.
