99 Ngày Spring — Ngày 65: Xử lý bất đồng bộ với @Async
@Async chuyển lời gọi method của Spring bean sang executor và trả quyền điều khiển sớm cho caller. Tính năng này hữu ích cho công việc độc lập, nhưng cần chọn pool, xử lý lỗi và hiểu proxy. Trên Spring Boot 3.x với Java 21, chỉ thêm annotation mà bỏ qua những điều đó thường tạo lỗi khó nhìn thấy.

1. Bật và cấu hình executor riêng
@EnableAsync bật xử lý annotation. Đặt ThreadPoolTaskExecutor riêng cho tác vụ email, có core/max, queue capacity và prefix để dễ theo dõi. Khi đầy cả pool và queue, chính sách từ chối phải phù hợp nghiệp vụ; đừng mặc nhiên bỏ task. Bean được Spring quản lý sẽ đóng theo vòng đời context.
@Configuration @EnableAsync
class AsyncConfig {
@Bean("mailExecutor")
ThreadPoolTaskExecutor mailExecutor() {
var e = new ThreadPoolTaskExecutor();
e.setCorePoolSize(2);
e.setMaxPoolSize(4);
e.setQueueCapacity(100);
e.setThreadNamePrefix("mail-");
return e;
}
}2. Trả CompletableFuture để giữ lỗi
Method @Async có thể trả void, Future hoặc CompletableFuture. Với CompletableFuture, Spring gọi method trên worker rồi chuyển kết quả/ngoại lệ sang future mà caller nhận. Trong method, completedFuture là giá trị tạm đã tính xong trên worker; caller sẽ nhận future proxy tạo. Với một lệnh gửi email thật, hãy cân nhắc retry/outbox nếu không được phép mất công việc khi process chết.
@Service
class MailService {
@Async("mailExecutor")
public CompletableFuture<String> send(long orderId) {
String receipt = deliver(orderId); // runs on mail worker
return CompletableFuture.completedFuture(receipt);
}
}3. Ngoại lệ của void task và cách chờ
Với method trả Future/CompletableFuture, caller có thể quan sát lỗi ở get(), join(), handle() hoặc exceptionally(). Với void, exception không quay lại caller; cấu hình AsyncUncaughtExceptionHandler qua AsyncConfigurer, hoặc ít nhất ghi log và metric. Không chặn bằng join() ngay sau khi gọi @Async nếu không cần, vì như vậy mất lợi ích bất đồng bộ.
CompletableFuture<String> sent = mailService.send(orderId);
sent.whenComplete((receipt, error) -> {
if (error != null) log.error("mail failed", error);
});4. Proxy, transaction và context
Chỉ lời gọi đi qua Spring proxy mới kích hoạt @Async; this.send() trong cùng bean vẫn chạy đồng bộ. Transaction của caller không tự mang sang worker thread, vì vậy đừng mong đọc dữ liệu chưa commit; hãy phát công việc sau commit hoặc dùng outbox. SecurityContext, MDC và ThreadLocal cũng không tự đi theo — truyền dữ liệu cần thiết rõ ràng hoặc cấu hình context propagation có kiểm soát.
Bài tập: in thread name ở caller và worker, rồi thử self-invocation.
Bài tập: làm send() ném lỗi và quan sát khác biệt giữa CompletableFuture và void.
@Async là cách dispatch một lời gọi trong ứng dụng, không phải hàng đợi bền vững. Chọn executor có giới hạn, theo dõi lỗi và nghĩ đến ranh giới transaction trước khi dùng cho nghiệp vụ quan trọng. Ngày 66 chuyển sang gọi API ngoài bằng RestClient và WebClient.
Tài liệu đối chiếu
https://docs.spring.io/spring-framework/reference/integration/scheduling.html#scheduling-annotation-support-async
https://docs.spring.io/spring-framework/reference/integration/scheduling.html#scheduling-annotation-support-async-exception
Site Admin
Engineer and writer. Building things with TypeScript and distributed systems.
Bình luận (0)
Bạn cần đăng nhập bằng Google để bình luận.
Hãy là người bình luận đầu tiên.


