observer pattern

- Published on
- /9 mins read/
Trong các cuốn giáo trình lập trình hướng đối tượng, Observer Pattern thường được minh họa bằng ví dụ trạm thời tiết (WeatherStation) cập nhật nhiệt độ lên màn hình hiển thị. Nhưng khi đưa vào môi trường Production của các hệ thống tài chính hay thương mại điện tử, việc sử dụng Observer đồng bộ ngây thơ có thể làm tê liệt toàn bộ luồng giao dịch: Một Observer gửi email bị timeout 30 giây sẽ khóa chặt luồng của người dùng, hoặc một Observer ghi log bị lỗi sẽ làm rollback oan uổng toàn bộ đơn hàng vừa thanh toán thành công!
Sự tiến hóa từ Observer Pattern cục bộ trong bộ nhớ (In-Process) sang Kiến trúc Hướng Sự kiện (Event-Driven Architecture - EDA) và Luồng phản ứng (Reactive Streams) là một trong những bước chuyển mình quan trọng nhất của kỹ thuật phần mềm hiện đại.
Bài viết này phân tích chi tiết lý do tại sao JDK 9 lại chính thức khai tử java.util.Observable, phân tích cơ chế quản lý giao dịch với @TransactionalEventListener trong Spring Boot, và thiết lập ranh giới kiến trúc khi nào nên dùng In-Process Event vs Distributed Message Broker (Kafka/RabbitMQ).
# các thế hệ tiến hóa của Observer pattern
# tại sao Java 9 lại deprecate Java.util.observable?
- Không thể mở rộng (Poor Extensibility):
Observablelà mộtclasschứ không phảiinterface. Do Java không hỗ trợ đa kế thừa (Multiple Inheritance), nếu class của bạn đã kế thừa một class khác, bạn không thể trở thànhObservable. - Không Thread-Safe & Rò rỉ bộ nhớ: Cơ chế đồng bộ bên trong của
Observablekhông bảo vệ được thứ tự thông báo, và danh sách observers giữ các strong references gây rò rỉ bộ nhớ nếu không hủy đăng ký (Memory Leak). - Thiếu ngữ nghĩa phân biệt: Mọi thông báo chỉ truyền một tham số vô kiểu
Object arg, làm mất hoàn toàn tính Type-Safety.
# cạm bẫy sống còn: ngoại lệ & quản lý giao dịch trong Spring
Khi một sự kiện nghiệp vụ xảy ra (ví dụ: OrderPlacedEvent), chúng ta có nhiều Observers cùng lắng nghe:
- Observer 1: Trừ số lượng tồn kho (Inventory Service - Cần chung Transaction với đơn hàng).
- Observer 2: Gửi email xác nhận (Notification Service - Gọi SMTP bên ngoài).
- Observer 3: Tích điểm khách hàng (Loyalty Service).
# giải pháp kiến trúc: @transactionaleventlistener kết hợp @async
Để bảo vệ tính toàn vẹn của nghiệp vụ, các tác vụ phụ trợ (gửi mail, đẩy thông báo) tuyệt đối không được làm ảnh hưởng đến Transaction chính:
@Service
public class OrderService {
private final ApplicationEventPublisher eventPublisher;
private final OrderRepository orderRepository;
@Transactional
public Order placeOrder(OrderRequest request) {
Order order = orderRepository.save(new Order(request));
// Phát sự kiện nhưng CHƯA thực thi ngay các listeners sau commit
eventPublisher.publishEvent(new OrderPlacedEvent(order.getId(), order.getTotalAmount()));
return order;
}
}@Component
public class OrderNotificationListeners {
private static final Logger log = LoggerFactory.getLogger(OrderNotificationListeners.class);
// KỸ THUẬT AN TOÀN: Chỉ chạy SAU KHI Transaction Database đã COMMIT thành công!
@Async("notificationExecutor") // Chạy bất đồng bộ trên thread pool riêng, không chặn người dùng
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void onOrderPlaced(OrderPlacedEvent event) {
log.info("Bắt đầu gửi email cho đơn hàng đã commit an toàn: {}", event.orderId());
try {
// Gọi SMTP Server bên ngoài
emailClient.sendOrderConfirmation(event.orderId());
} catch (Exception ex) {
// Lỗi gửi mail chỉ ghi log, KHÔNG THỂ rollback được đơn hàng của khách!
log.error("Không thể gửi email cho đơn hàng: {}", event.orderId(), ex);
}
}
}# vấn nạn tràn bộ đệm: backpressure & reactive streams
Khi chuyển sang mô hình Observer bất đồng bộ, một hiểm họa mới xuất hiện: Fast Producer vs Slow Consumer.
Nếu Subject bắn ra 10.000 events/giây, trong khi Observer chỉ có khả năng xử lý 500 events/giây:
- Hàng đợi bộ nhớ (In-Memory Queue) của Observer sẽ phình to khủng khiếp.
- Hệ thống cạn kiệt Heap Memory và sập với lỗi
OutOfMemoryError.
# kiến trúc reactive streams (Java 9 flow API / project Reactor)
Khác với Observer truyền thống là Push-based (Subject đẩy dữ liệu bất chấp Observer có chịu nổi hay không), Reactive Streams chuyển thành Pull-based kết hợp Dynamic Demand:
- Subscriber nhận một đối tượng
Subscription. - Subscriber ra lệnh:
subscription.request(n)— "Hãy đưa cho tôi n phần tử tiếp theo, khi nào xử lý xong tôi sẽ yêu cầu tiếp". - Nhờ vậy, hệ thống không bao giờ bị tràn bộ nhớ dù tốc độ sản sinh dữ liệu có nhanh đến mức nào!
# ma trận quyết định: in-process vs distributed message broker
Khi nào nên dùng Spring ApplicationEvents trong bộ nhớ, và khi nào bắt buộc phải đưa lên Kafka / RabbitMQ?
# bảng so sánh chi tiết
| Tiêu chí | Spring ApplicationEvents (In-Process) | Apache Kafka / RabbitMQ (Distributed) |
|---|---|---|
| Phạm vi | Nội bộ trong 1 JVM Process duy nhất | Xuyên suốt toàn bộ hệ thống phân tán |
| Độ trễ (Latency) | Cực thấp (< 1ms) (Truyền con trỏ bộ nhớ) | ~5ms - 20ms (Ghi disk, truyền mạng TCP) |
| Tính bền bỉ (Durability) | Mất khi Pod/JVM bị crash | Bền vững trên đĩa (Disk Persistence), Replicated |
| Độ phức tạp hạ tầng | Bằng 0 (Có sẵn trong Spring Framework) | Cao (Cần vận hành cụm Cluster, ZooKeeper/KRaft) |
| Transaction Boundary | Tích hợp sâu với @Transactional DB cục bộ | Cần dùng Transactional Outbox Pattern |
# bảng kiểm tra sẵn sàng vận hành (architect's checklist)
- 1. Bảo Vệ Giao Dịch Bằng
AFTER_COMMIT: Toàn bộ các tác vụ phụ trợ (Gửi mail, gọi bên thứ ba, ghi log ngoài) phải dùng@TransactionalEventListener(phase = AFTER_COMMIT). - 2. Phân Bổ Thread Pool Cho
@Async: Luôn chỉ định rõ tên Executor (ví dụ:@Async("eventExecutor")) với dung lượng queue có giới hạn (BoundedQueue), không dùng thread pool mặc địnhSimpleAsyncTaskExecutor. - 3. Xử Lý Ngoại Lệ Trong Listeners: Đảm bảo toàn bộ listener bất đồng bộ có khối
try-catchbao bọc và ghi log rõ ràng; không để unhandled exceptions làm chết worker thread. - 4. Hạn Chế Lưu Trữ Event Toàn Cục: Các event classes phải là immutable (sử dụng Java Record) để tránh việc một Listener sửa đổi dữ liệu làm sai lệch các Listener khác.
- 5. Giám Sát Execution Lag: Gắn metric đo lường số lượng event đang chờ trong hàng đợi bất đồng bộ để cảnh báo khi hệ thống có dấu hiệu quá tải.
# lời kết
Observer Pattern không hề lỗi thời; nó đã lột xác và hòa vào dòng chảy của kiến trúc hiện đại. Từ một mẫu thiết kế gom danh sách con trỏ đơn giản, nó đã trở thành nền tảng của Spring Application Events, Reactive Streams và toàn bộ thế giới Event-Driven Architecture.
Hiểu rõ ranh giới giữa luồng thực thi đồng bộ và bất đồng bộ, nắm vững cơ chế neo giữ giao dịch với @TransactionalEventListener, và biết khi nào nên dừng lại ở In-Process Event trước khi vội vã triển khai Kafka chính là biểu hiện của một Kiến trúc sư Phần mềm thực thụ: Luôn chọn giải pháp vừa đủ, an toàn và tối ưu nhất cho bài toán của doanh nghiệp.
Chỉ là những ghi chép cá nhân với hy vọng mang lại chút giá trị. Nếu thấy hữu ích, đừng ngại chia sẻ cho bạn bè & đồng nghiệp nhé!
Happy coding 😎 👍🏻 🚀 🔥.
On this page
- # các thế hệ tiến hóa của Observer pattern
- # tại sao Java 9 lại deprecate Java.util.observable?
- # cạm bẫy sống còn: ngoại lệ & quản lý giao dịch trong Spring
- # giải pháp kiến trúc: @transactionaleventlistener kết hợp @async
- # vấn nạn tràn bộ đệm: backpressure & reactive streams
- # kiến trúc reactive streams (Java 9 flow API / project Reactor)
- # ma trận quyết định: in-process vs distributed message broker
- # bảng so sánh chi tiết
- # bảng kiểm tra sẵn sàng vận hành (architect's checklist)
- # lời kết