Trong các ứng dụng thực tế, không phải mọi thao tác đều cần xảy ra đồng bộ. Gửi email, tạo báo cáo, xử lý tải file, và đồng bộ dữ liệu với hệ thống bên ngoài là những tác vụ có thể — và nên — chạy trong nền.
Spring Boot cung cấp hỗ trợ mạnh mẽ tích hợp sẵn cho xử lý bất đồng bộ thông qua annotation @Async và CompletableFuture, cùng với thực thi tác vụ theo lịch với @Scheduled. Kết hợp lại, những công cụ này cho phép bạn xây dựng các ứng dụng phản hồi nhanh, hiệu quả, xử lý các thao tác kéo dài mà không chặn request của người dùng.
Những Gì Bạn Sẽ Học:
✅ Bật và cấu hình xử lý async với @Async
✅ Trả về kết quả từ phương thức async với CompletableFuture
✅ Cấu hình thread pool tùy chỉnh cho production
✅ Scheduled tasks với @Scheduled và cron expressions
✅ Xử lý lỗi trong quy trình async
✅ Kết hợp nhiều thao tác async
✅ Xây dựng dịch vụ thông báo nền
package com.example.async.config;import org.springframework.context.annotation.Configuration;import org.springframework.scheduling.annotation.EnableAsync;@Configuration@EnableAsyncpublic class AsyncConfig { // @EnableAsync kích hoạt khả năng thực thi async của Spring // Mặc định, nó sử dụng SimpleAsyncTaskExecutor (không tái sử dụng thread) // Chúng ta sẽ cấu hình thread pool phù hợp ở bước tiếp theo}
Bước 2: Tạo Async Service Cơ Bản
package com.example.async.service;import lombok.extern.slf4j.Slf4j;import org.springframework.scheduling.annotation.Async;import org.springframework.stereotype.Service;@Service@Slf4jpublic class NotificationService { @Async public void sendEmail(String to, String subject, String body) { log.info("Sending email to {} on thread: {}", to, Thread.currentThread().getName()); try { // Mô phỏng gửi email chậm Thread.sleep(2000); log.info("Email sent successfully to {}", to); } catch (InterruptedException e) { Thread.currentThread().interrupt(); log.error("Email sending interrupted for {}", to); } } @Async public void sendSms(String phoneNumber, String message) { log.info("Sending SMS to {} on thread: {}", phoneNumber, Thread.currentThread().getName()); try { Thread.sleep(1000); log.info("SMS sent successfully to {}", phoneNumber); } catch (InterruptedException e) { Thread.currentThread().interrupt(); log.error("SMS sending interrupted for {}", phoneNumber); } }}
Bước 3: Gọi Phương Thức Async Từ Controller
package com.example.async.controller;import com.example.async.service.NotificationService;import lombok.RequiredArgsConstructor;import lombok.extern.slf4j.Slf4j;import org.springframework.http.ResponseEntity;import org.springframework.web.bind.annotation.*;import java.util.Map;@RestController@RequestMapping("/api/users")@RequiredArgsConstructor@Slf4jpublic class UserController { private final NotificationService notificationService; @PostMapping("/register") public ResponseEntity<Map<String, String>> registerUser(@RequestBody Map<String, String> user) { String email = user.get("email"); String phone = user.get("phone"); String name = user.get("name"); log.info("Registering user: {} on thread: {}", name, Thread.currentThread().getName()); // Lưu user vào database (đồng bộ) // userRepository.save(user); // Gửi thông báo (bất đồng bộ - trả về ngay lập tức) notificationService.sendEmail(email, "Welcome!", "Welcome to our platform, " + name); notificationService.sendSms(phone, "Your account has been created."); log.info("Registration complete for {} (notifications sent in background)", name); return ResponseEntity.ok(Map.of( "message", "User registered successfully", "email", email )); }}
Registering user: John on thread: http-nio-8080-exec-1Registration complete for John (notifications sent in background)Sending email to [email protected] on thread: task-1Sending SMS to +1234567890 on thread: task-2Email sent successfully to [email protected]SMS sent successfully to +1234567890
Lưu ý response trả về trước khi email và SMS được gửi. Các thông báo chạy trên các thread khác nhau (task-1, task-2).
Quan trọng: Phương thức @Async phải được gọi từ một class khác. Gọi phương thức @Async từ trong cùng class sẽ bỏ qua proxy và chạy đồng bộ. Điều này vì Spring sử dụng AOP proxy để chặn cuộc gọi và chuyển nó sang thread riêng.
Trả Về Kết Quả Với CompletableFuture
Phương thức @Async có thể trả về CompletableFuture<T> khi bạn cần kết quả sau:
Async Service Với Giá Trị Trả Về
package com.example.async.service;import lombok.extern.slf4j.Slf4j;import org.springframework.scheduling.annotation.Async;import org.springframework.stereotype.Service;import java.util.concurrent.CompletableFuture;@Service@Slf4jpublic class DataAggregationService { @Async public CompletableFuture<String> fetchUserProfile(Long userId) { log.info("Fetching user profile for {} on thread: {}", userId, Thread.currentThread().getName()); try { Thread.sleep(1000); // Mô phỏng gọi API bên ngoài } catch (InterruptedException e) { Thread.currentThread().interrupt(); } return CompletableFuture.completedFuture("User-" + userId + " profile data"); } @Async public CompletableFuture<String> fetchUserOrders(Long userId) { log.info("Fetching orders for {} on thread: {}", userId, Thread.currentThread().getName()); try { Thread.sleep(1500); // Mô phỏng truy vấn database } catch (InterruptedException e) { Thread.currentThread().interrupt(); } return CompletableFuture.completedFuture("User-" + userId + " has 5 orders"); } @Async public CompletableFuture<String> fetchUserRecommendations(Long userId) { log.info("Fetching recommendations for {} on thread: {}", userId, Thread.currentThread().getName()); try { Thread.sleep(2000); // Mô phỏng gọi ML service } catch (InterruptedException e) { Thread.currentThread().interrupt(); } return CompletableFuture.completedFuture("3 recommended products for User-" + userId); }}
Kết Hợp Nhiều Kết Quả Async
package com.example.async.controller;import com.example.async.service.DataAggregationService;import lombok.RequiredArgsConstructor;import lombok.extern.slf4j.Slf4j;import org.springframework.http.ResponseEntity;import org.springframework.web.bind.annotation.*;import java.util.Map;import java.util.concurrent.CompletableFuture;@RestController@RequestMapping("/api/dashboard")@RequiredArgsConstructor@Slf4jpublic class DashboardController { private final DataAggregationService dataService; @GetMapping("/{userId}") public ResponseEntity<Map<String, String>> getUserDashboard(@PathVariable Long userId) throws Exception { long start = System.currentTimeMillis(); // Chạy cả ba cuộc gọi song song CompletableFuture<String> profileFuture = dataService.fetchUserProfile(userId); CompletableFuture<String> ordersFuture = dataService.fetchUserOrders(userId); CompletableFuture<String> recommendationsFuture = dataService.fetchUserRecommendations(userId); // Chờ tất cả hoàn thành CompletableFuture.allOf(profileFuture, ordersFuture, recommendationsFuture).join(); long duration = System.currentTimeMillis() - start; log.info("Dashboard loaded in {}ms (sequential would be ~4500ms)", duration); return ResponseEntity.ok(Map.of( "profile", profileFuture.get(), "orders", ordersFuture.get(), "recommendations", recommendationsFuture.get(), "loadTimeMs", String.valueOf(duration) )); }}
So sánh hiệu suất:
Tuần tự: 1000ms + 1500ms + 2000ms = 4500msSong song async: max(1000, 1500, 2000) = ~2000ms (nhanh hơn 2.25 lần)
Chuỗi Thao Tác CompletableFuture
@GetMapping("/{userId}/summary")public CompletableFuture<ResponseEntity<Map<String, String>>> getUserSummary(@PathVariable Long userId) { return dataService.fetchUserProfile(userId) .thenCombine(dataService.fetchUserOrders(userId), (profile, orders) -> { // Kết hợp profile + orders return Map.of("profile", profile, "orders", orders); }) .thenApply(result -> { // Chuyển đổi kết quả kết hợp return ResponseEntity.ok(result); }) .exceptionally(ex -> { // Xử lý lỗi return ResponseEntity.internalServerError() .body(Map.of("error", ex.getMessage())); });}
Cấu Hình Thread Pool Tùy Chỉnh
SimpleAsyncTaskExecutor mặc định tạo thread mới cho mỗi tác vụ — điều này không phù hợp cho production. Hãy cấu hình thread pool đúng cách:
Thread Pool Sẵn Sàng Production
package com.example.async.config;import lombok.extern.slf4j.Slf4j;import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.scheduling.annotation.AsyncConfigurer;import org.springframework.scheduling.annotation.EnableAsync;import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;import java.util.concurrent.Executor;@Configuration@EnableAsync@Slf4jpublic class AsyncConfig implements AsyncConfigurer { @Override @Bean(name = "taskExecutor") public Executor getAsyncExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); // Core pool size: số thread duy trì ngay cả khi idle executor.setCorePoolSize(5); // Max pool size: số thread tối đa khi queue đầy executor.setMaxPoolSize(10); // Queue capacity: tác vụ chờ khi core threads bận executor.setQueueCapacity(100); // Tiền tố tên thread để debug dễ hơn executor.setThreadNamePrefix("async-"); // Xử lý khi pool và queue đều đầy executor.setRejectedExecutionHandler((r, e) -> log.error("Task rejected: thread pool and queue are full") ); // Chờ tác vụ hoàn thành khi shutdown executor.setWaitForTasksToCompleteOnShutdown(true); executor.setAwaitTerminationSeconds(30); executor.initialize(); return executor; } @Override public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() { return (ex, method, params) -> log.error("Async error in method {}: {}", method.getName(), ex.getMessage(), ex); }}
Nhiều Thread Pool Cho Các Workload Khác Nhau
Cô lập các workload khác nhau với pool riêng biệt:
@Configuration@EnableAsyncpublic class AsyncConfig { @Bean(name = "emailExecutor") public Executor emailExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(2); executor.setMaxPoolSize(5); executor.setQueueCapacity(50); executor.setThreadNamePrefix("email-"); executor.initialize(); return executor; } @Bean(name = "reportExecutor") public Executor reportExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(3); executor.setMaxPoolSize(8); executor.setQueueCapacity(25); executor.setThreadNamePrefix("report-"); executor.initialize(); return executor; } @Bean(name = "notificationExecutor") public Executor notificationExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(2); executor.setMaxPoolSize(4); executor.setQueueCapacity(200); executor.setThreadNamePrefix("notify-"); executor.initialize(); return executor; }}
Tham chiếu pool cụ thể với giá trị @Async:
@Async("emailExecutor")public void sendEmail(String to, String subject, String body) { // Chạy trên emailExecutor pool}@Async("reportExecutor")public CompletableFuture<byte[]> generateReport(Long reportId) { // Chạy trên reportExecutor pool return CompletableFuture.completedFuture(new byte[0]);}
Hướng Dẫn Kích Thước Thread Pool
Loại Workload
Core Size
Max Size
Queue
Ví Dụ
CPU-bound
# CPUs
# CPUs
Nhỏ (10-50)
Xử lý dữ liệu
I/O-bound
# CPUs × 2
# CPUs × 4
Lớn (100-500)
HTTP calls, email
Hỗn hợp
# CPUs
# CPUs × 2
Trung bình (50-100)
Async chung
Scheduled Tasks Với @Scheduled
Annotation @Scheduled của Spring cho phép bạn chạy phương thức theo khoảng thời gian cố định hoặc lịch cron.
Bước 1: Bật Scheduling
package com.example.async.config;import org.springframework.context.annotation.Configuration;import org.springframework.scheduling.annotation.EnableScheduling;@Configuration@EnableSchedulingpublic class SchedulingConfig {}
Bước 2: Tạo Scheduled Tasks
package com.example.async.service;import lombok.extern.slf4j.Slf4j;import org.springframework.scheduling.annotation.Scheduled;import org.springframework.stereotype.Service;import java.time.LocalDateTime;import java.time.format.DateTimeFormatter;@Service@Slf4jpublic class ScheduledTaskService { /** * Fixed rate: chạy mỗi 60 giây bất kể thời gian thực thi trước */ @Scheduled(fixedRate = 60000) public void cleanExpiredSessions() { log.info("Cleaning expired sessions at {}", now()); // sessionRepository.deleteExpired(); } /** * Fixed delay: chờ 30 giây SAU KHI thực thi trước hoàn thành */ @Scheduled(fixedDelay = 30000) public void syncExternalData() { log.info("Syncing external data at {}", now()); // externalApiClient.sync(); } /** * Initial delay: chờ 10 giây trước lần thực thi đầu, * sau đó chạy mỗi 5 phút */ @Scheduled(fixedRate = 300000, initialDelay = 10000) public void refreshCache() { log.info("Refreshing cache at {}", now()); // cacheService.refreshAll(); } /** * Cron expression: chạy lúc 9:00 sáng mỗi thứ Hai * Format: giây phút giờ ngày-trong-tháng tháng ngày-trong-tuần */ @Scheduled(cron = "0 0 9 * * MON") public void generateWeeklyReport() { log.info("Generating weekly report at {}", now()); // reportService.generateWeekly(); } /** * Cron: chạy mỗi ngày lúc nửa đêm */ @Scheduled(cron = "0 0 0 * * *") public void dailyCleanup() { log.info("Running daily cleanup at {}", now()); // cleanupService.runDaily(); } /** * Cron: chạy mỗi 15 phút trong giờ làm việc (Thứ 2-6, 9AM-5PM) */ @Scheduled(cron = "0 */15 9-17 * * MON-FRI") public void checkHealthDuringBusinessHours() { log.info("Health check at {}", now()); // healthService.check(); } private String now() { return LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); }}
Tham Khảo Cron Expression
┌───────────── giây (0-59)│ ┌───────────── phút (0-59)│ │ ┌───────────── giờ (0-23)│ │ │ ┌───────────── ngày trong tháng (1-31)│ │ │ │ ┌───────────── tháng (1-12)│ │ │ │ │ ┌───────────── ngày trong tuần (0-7, SUN-SAT)│ │ │ │ │ │* * * * * *
// SAI - @Async bị bỏ qua (không có proxy)@Servicepublic class MyService { public void doWork() { asyncMethod(); // Chạy đồng bộ! } @Async public void asyncMethod() { }}// ĐÚNG - Gọi từ bean khác@Servicepublic class CallerService { private final AsyncService asyncService; public void doWork() { asyncService.asyncMethod(); // Chạy bất đồng bộ }}
Thiếu @EnableAsync:
// @Async không có tác dụng nếu thiếu @EnableAsync trên class @Configuration
Không xử lý InterruptedException đúng cách:
// ĐÚNGcatch (InterruptedException e) { Thread.currentThread().interrupt(); // Khôi phục interrupt flag throw new RuntimeException("Task interrupted", e);}
Tóm Tắt và Điểm Chính
Bạn đã học cách xây dựng các ứng dụng bất đồng bộ, non-blocking với Spring Boot.
Những Gì Đã Học:
✅ @Async cho tác vụ nền fire-and-forget
✅ CompletableFuture cho phương thức async có giá trị trả về
✅ Cấu hình thread pool tùy chỉnh cho production
✅ Nhiều thread pool cho cô lập workload
✅ @Scheduled cho fixed-rate, fixed-delay, và cron tasks
✅ Xử lý lỗi và mẫu retry
✅ Xây dựng dịch vụ thông báo nền
✅ Kiểm thử code async và scheduled
Quy Tắc Chính:
Luôn cấu hình ThreadPoolTaskExecutor phù hợp cho production
Gọi phương thức @Async từ bean khác (hạn chế proxy)
Sử dụng CompletableFuture khi cần kết quả từ thao tác async
Xử lý InterruptedException đúng cách — khôi phục interrupt flag
Externalize cron expressions qua properties để linh hoạt
Bước Tiếp Theo:
Thực hành: Thêm async email notifications vào dự án hiện có
Khám phá: Tìm hiểu Spring Events (@EventListener) cho xử lý async decoupled
Nâng cấp: Học message queues (RabbitMQ, Kafka) cho xử lý async phân tán
Giám sát: Thêm thread pool metrics vào Actuator dashboards
Câu Hỏi Thường Gặp
Q: Sự khác biệt giữa fixedRate và fixedDelay là gì?
A: fixedRate kích hoạt ở khoảng thời gian cố định bất kể thời gian thực thi. Nếu tác vụ mất 3 giây và rate là 5 giây, lần chạy tiếp theo bắt đầu 2 giây sau khi lần trước kết thúc. fixedDelay chờ thời gian được chỉ định sau khi lần thực thi trước hoàn thành — nên delay 5 giây luôn nghĩa là 5 giây idle giữa các lần chạy.
Q: Có thể dùng @Async với @Transactional không?
A: Có, nhưng lưu ý phương thức async chạy trong thread riêng với transaction context riêng. Transaction của phương thức gọi sẽ không truyền sang phương thức async. Nếu cần transactional behavior, đánh dấu @Transactional trên chính phương thức async.
Q: Nên cấu hình bao nhiêu thread?
A: Cho tác vụ I/O-bound (HTTP calls, email, database), bắt đầu với 2× CPU cores cho core size và 4× cho max. Cho tác vụ CPU-bound, đặt core size bằng số CPU. Giám sát dưới tải và điều chỉnh.
Q: Điều gì xảy ra khi thread pool đầy?
A: Mặc định, tác vụ đi vào queue. Khi queue cũng đầy, RejectedExecutionHandler được gọi. Các tùy chọn bao gồm: AbortPolicy (throw exception), CallerRunsPolicy (chạy trên calling thread), DiscardPolicy (bỏ âm thầm), hoặc handler tùy chỉnh.
Q: Nên dùng @Async hay message queue như RabbitMQ?
A: Dùng @Async cho tác vụ nền đơn giản trong cùng process. Dùng message queue khi cần: persistence tác vụ qua restart, xử lý phân tán trên nhiều server, delivery đảm bảo, hoặc routing phức tạp. Message queues thêm độ phức tạp vận hành nhưng cung cấp đảm bảo mạnh hơn.