99 Ngày Spring — Ngày 71: Microservices tổng quan
Bảy mươi ngày qua, mọi thứ ta xây đều nằm trong một ứng dụng Spring Boot. Chặng mới này nói về hệ thống gồm nhiều ứng dụng: microservices và Spring Cloud. Nhưng bài đầu tiên của chặng không có Spring Cloud. Nó trả lời câu hỏi phải có trước mọi công cụ: tách ra thì được gì, phải trả giá gì, và khi nào không nên tách. Ví dụ dùng Spring Boot 3.5 và Java 21.

1. Monolith không phải là một lời chê
Monolith là hệ thống được đóng gói và triển khai thành một khối: một file jar, một tiến trình, thường là một database. Các phần của hệ thống gọi nhau bằng lời gọi hàm trong cùng bộ nhớ. Đó là điểm mạnh, không phải điểm yếu.
package vn.motdev.shop.order;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class OrderService {
private final OrderRepository orders;
private final InventoryService inventory; // cùng tiến trình: lời gọi hàm, vài nano-giây
private final PaymentService payment;
OrderService(OrderRepository orders, InventoryService inventory, PaymentService payment) {
this.orders = orders; this.inventory = inventory; this.payment = payment;
}
@Transactional // MỘT transaction, MỘT database: trừ kho, thu tiền, lưu đơn — cùng commit hoặc cùng rollback
public Order place(PlaceOrder cmd) {
inventory.reserve(cmd.sku(), cmd.quantity());
payment.charge(cmd.customerId(), cmd.total());
return orders.save(Order.from(cmd));
}
}Lời gọi hàm trong cùng tiến trình không bao giờ timeout, không mất gói, không cần serialize. Lỗi của nó là exception, và stack trace chỉ đúng dòng.
Một transaction bao được cả ba bước. Trừ kho thành công mà thu tiền thất bại thì database tự rollback; bạn không viết thêm dòng nào.
Một lần build, một lần deploy, một nơi xem log. Debug là đặt breakpoint rồi bấm chạy.
Monolith hỏng khi nó thành một khối không có ranh giới: mọi lớp gọi mọi lớp, sửa một chỗ vỡ ba chỗ. Đó là vấn đề của cấu trúc mã, không phải của cách đóng gói.
2. Microservices thật sự là gì
Microservices là kiến trúc trong đó hệ thống gồm nhiều service nhỏ, mỗi service triển khai độc lập, do một đội sở hữu, giữ dữ liệu riêng và nói chuyện với nhau qua mạng. Hai chữ in đậm là phép thử: nếu muốn deploy service A mà phải deploy B cùng lúc, hoặc A đọc thẳng bảng của B, thì đó chưa phải microservices.
package vn.motdev.order;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestClient;
@Service
public class OrderService {
private final OrderRepository orders;
private final RestClient inventory; // tiến trình khác, máy khác, đội khác
private final RestClient payment;
OrderService(OrderRepository orders, RestClient inventoryClient, RestClient paymentClient) {
this.orders = orders; this.inventory = inventoryClient; this.payment = paymentClient;
}
// KHÔNG còn @Transactional bao cả ba bước: mỗi service có database riêng.
public Order place(PlaceOrder cmd) {
Order order = orders.save(Order.pending(cmd)); // 1. lưu trạng thái PENDING ở DB của mình
inventory.post().uri("/api/reservations") // 2. mạng: chậm? timeout? trả 503?
.body(new Reserve(order.id(), cmd.sku(), cmd.quantity()))
.retrieve().toBodilessEntity();
try {
payment.post().uri("/api/charges") // 3. kho đã trừ, giờ thu tiền
.body(new Charge(order.id(), cmd.customerId(), cmd.total()))
.retrieve().toBodilessEntity();
} catch (RuntimeException e) {
inventory.delete().uri("/api/reservations/{id}", order.id()) // bù trừ: trả kho
.retrieve().toBodilessEntity(); // ...và lời gọi bù trừ này cũng có thể lỗi
return orders.save(order.rejected());
}
return orders.save(order.confirmed());
}
}Lời gọi hàm đã thành lời gọi mạng. Nó có thể chậm, có thể timeout, có thể thành công ở bên kia nhưng phản hồi không về tới bên này.
Không còn transaction chung. Kho đã trừ mà thu tiền thất bại thì chính bạn phải viết bước bù trừ, và bước bù trừ cũng có thể lỗi. Đây là lý do có saga và outbox, những mẫu ta sẽ gặp ở các ngày sau.
Dữ liệu chỉ nhất quán sau cùng: có một khoảng thời gian đơn hàng đang PENDING trong khi kho đã bị giữ. Giao diện và nghiệp vụ phải chấp nhận trạng thái trung gian đó.
Đoạn mã trên còn thiếu timeout, retry và mã chống thực hiện trùng. Viết đủ thì dài gấp ba bản monolith, để làm đúng một việc như cũ.
3. Đánh đổi: được gì, trả gì
Microservices không làm hệ thống đơn giản hơn. Nó chuyển độ phức tạp từ bên trong mã nguồn sang hạ tầng và vận hành. Đáng hay không tùy vào việc bạn đang đau ở đâu.
Được: triển khai độc lập. Đội thanh toán phát hành mười lần một ngày mà không chờ đội kho. Đây là lợi ích lớn nhất, và nó là lợi ích về tổ chức, không phải về kỹ thuật.
Được: mở rộng riêng từng phần. Phần tìm kiếm cần hai mươi instance, phần báo cáo cần một. Không phải nhân cả khối lên.
Được: cô lập lỗi. Service gợi ý sản phẩm chết thì trang vẫn bán được hàng, với điều kiện bên gọi có timeout và phương án dự phòng.
Trả: mạng không đáng tin. Mỗi lời gọi cần timeout, retry có giới hạn, circuit breaker. Chuỗi gọi đồng bộ A sang B sang C nhân xác suất lỗi và cộng dồn độ trễ.
Trả: dữ liệu phân tán. Không join giữa hai service, không transaction chung. Một báo cáo đơn giản trước đây là một câu SQL, giờ là một bài toán tích hợp.
Trả: vận hành. Mười service là mười pipeline, mười dashboard, mười bộ cấu hình. Thiếu log tập trung và tracing (Ngày 70) thì một lỗi đi qua ba service gần như không lần ra được.
Trả: kiểm thử và phiên bản. Đổi một API phải nghĩ tới mọi bên đang gọi nó. Kiểm thử tích hợp cần dựng nhiều service cùng lúc.
4. Khi nào KHÔNG nên
Đội nhỏ. Năm người không có vấn đề phối hợp mà microservices giải quyết, nhưng sẽ gánh toàn bộ chi phí vận hành của nó.
Chưa rõ ranh giới nghiệp vụ. Cắt sai ranh giới trong monolith thì sửa bằng một lần refactor. Cắt sai giữa các service thì phải chuyển dữ liệu và đổi hợp đồng API giữa các đội.
Chưa có nền vận hành. Chưa có CI/CD tự động, log tập trung, giám sát và cảnh báo thì mỗi service thêm vào là thêm một nơi hỏng trong im lặng.
Cần nhất quán mạnh ở mọi nơi. Nếu gần như thao tác nào cũng phải cùng thành công hoặc cùng thất bại, hệ thống đó đang nói rằng nó muốn một database.
Dấu hiệu của monolith phân tán, thứ gánh chi phí của cả hai phía: các service dùng chung database, phải deploy cùng nhau, và một request đi qua năm lời gọi đồng bộ nối tiếp.
5. Con đường hợp lý: monolith có module trước
Lựa chọn không phải là khối hỗn độn hay hai mươi service. Ở giữa có modular monolith: vẫn một ứng dụng, một lần deploy, nhưng mã được chia thành module theo nghiệp vụ với ranh giới được kiểm tra tự động. Khi một module thật sự cần đội riêng hoặc nhịp phát hành riêng, bạn tách đúng module đó ra, lúc ranh giới đã được chứng minh.
shop/ # MỘT ứng dụng Spring Boot, MỘT file jar
└── src/main/java/vn/motdev/shop/
├── ShopApplication.java
├── order/ # module = package cấp một
│ ├── OrderService.java # API công khai của module
│ └── internal/ # mọi thứ còn lại: module khác không được chạm
├── inventory/
│ ├── InventoryService.java
│ └── internal/
└── payment/
├── PaymentService.java
└── internal/package vn.motdev.shop;
import org.junit.jupiter.api.Test;
import org.springframework.modulith.core.ApplicationModules;
class ModularityTests {
@Test
void modulesRespectTheirBoundaries() {
// dependency (test): org.springframework.modulith:spring-modulith-starter-test
// Thất bại nếu order chạm vào inventory.internal, hoặc hai module phụ thuộc vòng nhau.
ApplicationModules.of(ShopApplication.class).verify();
}
}Spring Modulith coi mỗi package cấp một là một module, và mọi thứ trong package con là nội bộ của module. Bài test trên thất bại ngay khi có module chạm vào phần nội bộ của module khác.
Module nói chuyện qua API công khai hoặc qua application event (Ngày 63). Dùng event thì khi tách ra thành service, phần lớn mã nghiệp vụ không phải viết lại, chỉ đổi kênh truyền.
Mỗi module nên sở hữu các bảng của mình ngay từ đầu, dù chung một database. Join chéo module hôm nay là sợi dây buộc hai service ngày mai.
Tách từng phần một, bắt đầu từ phần có lý do rõ nhất: tải khác hẳn, nhịp thay đổi khác hẳn, hoặc đã có đội riêng.
6. Bản đồ của chặng microservices
Mỗi ngày tiếp theo giải quyết một vấn đề chỉ xuất hiện khi đã có nhiều service. Thứ tự dưới đây cũng là thứ tự các vấn đề thường gõ cửa. Các ví dụ dùng Spring Cloud 2025.0, bản đi cùng Spring Boot 3.5.
Ngày 72 Spring Cloud Config cấu hình của 10 service về một nơi có version
Ngày 73 Service discovery service tìm nhau khi địa chỉ đổi liên tục (Eureka)
Ngày 74 API Gateway một cửa vào: route, filter, xác thực
Ngày 75 Giao tiếp giữa service RestClient, HTTP interface, retry an toàn
Ngày 76 Circuit breaker ngắt mạch khi đối tác chết (Resilience4j)7. Bài tập thực hành
Lấy hệ thống bạn đang làm, viết ra ba module nghiệp vụ và bảng nào thuộc module nào. Có bảng nào bị hai module cùng ghi không?
Trong ví dụ đặt hàng, liệt kê mọi điểm có thể hỏng của bản microservices mà bản monolith không có. Với mỗi điểm, hệ thống rơi vào trạng thái nào?
Thêm Spring Modulith vào một dự án Spring Boot có sẵn và chạy
ApplicationModules.of(...).verify(). Đọc danh sách vi phạm: đó là bản đồ ranh giới thật của mã.Với đội của bạn, viết một câu trả lời cho câu hỏi: vấn đề cụ thể nào ta đang gặp mà triển khai độc lập sẽ giải quyết?
Tóm lại: monolith cho bạn lời gọi hàm, một transaction và một lần deploy; microservices cho bạn triển khai độc lập và mở rộng riêng, với giá là mạng, dữ liệu phân tán và vận hành. Bắt đầu bằng monolith có module, tách khi có lý do đo được. Giả sử bạn đã có lý do đó: bài toán đầu tiên xuất hiện là cấu hình của mười service nằm ở mười nơi. Ngày 72 giải nó bằng Spring Cloud Config.
Tài liệu đối chiếu
Martin Fowler & James Lewis, Microservices: https://martinfowler.com/articles/microservices.html ; MonolithFirst: https://martinfowler.com/bliki/MonolithFirst.html ; Spring Modulith reference: https://docs.spring.io/spring-modulith/reference/ ; Spring Cloud: https://spring.io/projects/spring-cloud
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.


