Job scheduler batch Spring Boot sering dijalankan untuk mentransfer data antar sistem eksternal. Artikel ini langsung menjawab bagaimana mengidentifikasi dan memperbaiki deadlock yang terjadi saat scheduler tersebut melakukan transfer data: dari gejala timeout dan backlog, langkah reproduksi di lingkungan development, analisis root cause, sampai perbaikan konkret.
Konteks Sistem dan Arsitektur Scheduler
Tim membangun satu layanan Spring Boot yang mengambil data dari database SQL, memprosesnya, lalu mengirimkan hasil ke message broker. Scheduler dijalankan setiap 5 menit, memanggil service yang membuka transaksi, membaca batch dari tabel transfer_queue, dan menulis hasil ke tabel transfer_log sebelum mengirim pesan.
Arsitektur singkat:
- Scheduler:
@Scheduled(fixedDelay = 300000). - TransferService: 3 langkah—read & lock row, proccess business logic, mark as completed.
- Transaksi:
@Transactionaldengan isolation default database. - Thread pool: default
SimpleAsyncTaskExecutorkarena perintah async pada service layer.
Gejala dan Observasi
Gejala nyata di produksi:
- Scheduler melaporkan timeout karena
DataAccessExceptionsaat menulis log dan request menunggu lebih dari 30 detik. - Queue backlog meningkat dan transfer job mengendap.
- Thread dump menunjukkan beberapa thread sedang menunggu
java.sql.Connectiondan terjebak dalamsynchronizedblock internal. - Database menunjukkan banyak lock wait dengan query
SELECT ... FOR UPDATEdari job lain.
Observabilitas penting: gunakan metrics dari Spring Actuator dan SQL slow log untuk mengidentifikasi kapan scheduler mulai menumpuk.
Langkah Reproduksi Lokal
Reproduksi dibuat dengan:
- Membuat dua job scheduler paralel dengan
@AsyncdanfixedDelaysama. - Menjalankan
SELECT ... FOR UPDATEdalam transaction pada row overlapping. - Memaksa batch size besar sehingga satu transaksi memegang lock lama.
Dengan profiler (VisualVM atau jconsole), terlihat deadlock antar thread: satu menunggu transaksi SQL, lain menunggu synchronized pada cache batch sementara connection pool habis.
Analisis Root Cause
Root cause utama:
- Transaksi panjang karena proses batch besar dan log tambahan. Lock row untuk iterasi SQL menjaga agar data tidak diproses ganda.
- Thread pool terbatas akibat
SimpleAsyncTaskExecutoryang membuat task baru tanpa batas, menghabiskan connection pool. - Synchronized block pada cache batch (misalnya
synchronized (batchTracker)) menunggu thread lain yang ternyata sedang menunggu transaksi database.
Kombinasi lock database + synchronized menghasilkan deadlock: thread A memegang database lock, menunggu synchronized; thread B memegang synchronized, menunggu connection dari pool yang dipakai thread A.
Perbaikan Konkrit
1. Batasi ukuran batch dan kurangi durasi transaksi
Ubah query menjadi membatasi row per iterasi:
SELECT * FROM transfer_queue WHERE status = 'READY' ORDER BY created_at LIMIT :batchSize FOR UPDATE
Set batchSize menjadi 50-100 berdasarkan throughput. Setelah proses, commit transaksi sebelum memanggil service eksternal.
2. Atur transaction isolation dan timeout
Gunakan isolation level READ_COMMITTED untuk menghindari lock yang tidak perlu dan set timeout pendek:
@Transactional(isolation = Isolation.READ_COMMITTED, timeout = 10)
public void processBatch(...) { ... }
Transaction timeout mencegah thread menggantung terlalu lama.
3. Konfigurasi executor async yang bounded
Jangan mengandalkan SimpleAsyncTaskExecutor; definisikan bean executor dengan pool terbatas:
@Bean(name = "transferExecutor")
public ThreadPoolTaskExecutor transferExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(4);
executor.setMaxPoolSize(8);
executor.setQueueCapacity(20);
executor.setThreadNamePrefix("transfer-");
executor.initialize();
return executor;
}
Gunakan executor ini pada @Async("transferExecutor") agar connection pool tidak overcommit.
4. Minimalkan synchronized dan gunakan reentrant locks
Alih-alih synchronized di metode shared state, pertimbangkan ReentrantLock dengan tryLock dan timeout singkat agar tidak menunggu forever.
5. Observability dan alerting
Implementasikan metrics:
- Spring Actuator untuk
scheduled.task.running. - Micrometer meter untuk latency batch.
- Alert ketika backlog queue > threshold atau jumlah thread blocked tinggi.
Thread dump otomatis (misalnya dengan jcmd) membantu melihat deadlock.
Takeaways untuk Developer
- Deadlock scheduler sering disebabkan kombinasi lock database + lock JVM; observability harus menangkap kedua sisi.
- Batch besar perlu dibagi, transaksi dibuat pendek, dan executor dibatasi agar resource tidak habis.
- Gunakan isolation level yang sesuai dan set timeout agar transaksi tidak hang.
- Synchronized pada shared state harus dipertimbangkan kembali bila logika lain menunggu resource yang sama.
- Monitoring backlog dan thread state memungkinkan deteksi dini deadlock sebelum backlog memuncak.
Dengan mengevaluasi konfigurasi transaction, executor, dan observability, jadwal transfer data bisa berjalan stabil tanpa deadlock.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!