基本的な実装と課題
Spring Bootの@Scheduledアノテーションを使用する場合、デフォルトではシングルスレッドで処理が実行されます。この仕様により、処理時間が長いタスクが存在する場合、以下の2つの問題が発生します:
- タスクの実行遅延
- スレッドの競合による処理停止
@Component
@EnableScheduling
public class ScheduledTask {
@Scheduled(cron = "0/5 * * * * ?")
public void executeTask() {
try {
Thread.sleep(10000);
System.out.println("現在のスレッドID: " + Thread.currentThread().getId());
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
問題の根本原因
SpringのTaskSchedulingAutoConfigurationが生成するスレッドプールのデフォルト設定では、コアスレッド数が1に固定されているため、並列処理が不可能です。この設計は、軽量なタスクを想定していますが、実際の業務では処理時間が長くなるケースが多いため、適切なスレッドプールの構成が必要です。
解決策
1. スレッドプールの設定
application.ymlでスレッドプールのサイズを変更可能です:
spring:
task:
scheduling:
pool:
size: 10
thread-name-prefix: task-pool-
2. 非同期処理の導入
カスタムスレッドプールを作成し、非同期処理を実装します:
@Configuration
public class TaskPoolConfig {
@Bean
public ExecutorService taskExecutor() {
return new ThreadPoolTaskExecutor(10, 20, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<>(200));
}
}
3. 分散環境におけるロック処理
Redissonを使用して分散ロックを実装することで、複数ノード間でのタスク競合を防止できます:
@Component
@EnableScheduling
public class DistributedTask {
private static final String TASK_LOCK = "distributed:task-lock";
private final RedissonClient redisson;
public DistributedTask(RedissonClient redisson) {
this.redisson = redisson;
}
@Scheduled(cron = "0/5 * * * * ?")
public void runDistributedTask() {
RLock lock = redisson.getLock(TASK_LOCK);
try {
lock.lock(10, TimeUnit.SECONDS);
// 実際の処理
} finally {
lock.unlock();
}
}
}
実装上の留意点
- スレッドプールのサイズ設定は、システムリソースと処理量を考慮して決定する必要があります
- 非同期処理では、例外処理とリソース解放の明示的な管理が重要です
- 分散ロックの生存確認(リース時間)を適切に設定し、デッドロックを防止する必要があります