🧩🧠 Tách Service theo Database: cái bẫy lớn nhất của Microservices - Microservice Architecture P3
Tách Service theo Database: cái bẫy lớn nhất của Microservices
1. Ảo tưởng mang tên "Database-per-Service"
Trong thế giới thiết kế hệ thống phân tán, có một nguyên lý được xem là "luật bất thành văn": Database-per-Service (Mỗi dịch vụ một cơ sở dữ liệu). Bất kỳ tài liệu, cuốn sách hay khóa học nào về Microservices cũng đều nhấn mạnh rằng: để các dịch vụ thực sự độc lập, chúng phải sở hữu cơ sở dữ liệu của riêng mình. Không được dùng chung database, không được query trực tiếp vào bảng của nhau để tránh sự ràng buộc chặt chẽ (tight coupling) và loại bỏ các điểm nghẽn cổ chai (bottlenecks) về mặt hạ tầng.
Nghe rất hợp lý và sạch sẽ. Và thế là, nhiều đội ngũ phát triển khi chuyển đổi từ hệ thống nguyên khối (Monolith) sang Microservices đã lập tức áp dụng công thức này một cách máy móc:
- Bảng
userstrong Monolith? Tách thànhUserServicesở hữuuser_db. - Bảng
ordersvàorder_items? Tách thànhOrderServicesở hữuorder_db. - Bảng
products? Tách thànhProductServicesở hữuproduct_db.
Họ tin rằng việc tách database chỉ đơn thuần là tạo ra các database instance mới, chạy script migrate dữ liệu, rồi viết API bao bọc (wrap) xung quanh các bảng đó. Nhưng thực chất, đây chính là cái bẫy lớn nhất và dễ gieo rắc thảm họa nhất khi bước chân vào thế giới Microservices. Việc chia tách dữ liệu dựa trên các thực thể bảng vật lý (Entity-based splitting) mà thiếu đi tư duy nghiệp vụ là con đường ngắn nhất dẫn hệ thống của bạn đến "nghĩa địa" của những dự án thất bại.
2. Câu chuyện từ thực tế: Khi "tách đôi con đường" trở thành cơn ác mộng
Hãy cùng nhìn vào một câu chuyện thực tế tại một doanh nghiệp thương mại điện tử tầm trung. Hệ thống Monolith cũ chạy trên một cơ sở dữ liệu PostgreSQL duy nhất bắt đầu gặp giới hạn về hiệu năng khi lượng truy cập tăng. Để chuẩn bị cho chiến dịch Flash Sale lớn nhất năm, ban giám đốc quyết định đập bỏ Monolith và chuyển dịch sang Microservices.
Đội ngũ kỹ sư tiến hành tách cơ sở dữ liệu thành user_db (chứa bảng users, wallets) và order_db (chứa bảng orders, order_items, products). Về mặt lý thuyết, luồng đặt hàng cực kỳ đơn giản: Khi khách hàng thực hiện checkout, OrderService sẽ ghi nhận đơn hàng mới vào order_db. Tuy nhiên, để tạo được đơn hàng, hệ thống cần thực hiện các bước sau:
- Xác thực thông tin người dùng và kiểm tra số dư ví (Wallet Balance) trong
user_db. - Trừ tiền trong ví của người dùng tại
user_db. - Tạo đơn hàng và trừ số lượng tồn kho (Inventory) trong
order_db.
Hãy so sánh sự khác biệt trong cách xử lý giữa Monolith và Microservices qua đoạn mã giả dưới đây:
Trong Monolith (Giao dịch cục bộ đáng tin cậy):
# Monolith - Đảm bảo tính toàn vẹn bằng ACID cục bộ
def checkout_order(user_id, cart_items, cart_total):
try:
with db.transaction():
# 1. Kiểm tra và trừ tiền ví
user_wallet = db.query("SELECT balance FROM wallets WHERE user_id = ? FOR UPDATE", user_id)
if user_wallet.balance < cart_total:
raise InsufficientBalanceException()
db.execute("UPDATE wallets SET balance = balance - ? WHERE user_id = ?", cart_total, user_id)
# 2. Tạo đơn hàng
order_id = db.execute("INSERT INTO orders (user_id, total) VALUES (?, ?)", user_id, cart_total)
# 3. Trừ kho sản phẩm
for item in cart_items:
db.execute("UPDATE products SET stock = stock - ? WHERE id = ?", item.quantity, item.id)
except Exception as e:
# Nếu có bất kỳ lỗi nào xảy ra, toàn bộ dữ liệu tự động rollback
db.rollback()
raise e
Trong Microservices (Tách database theo bảng vật lý):
# Microservices - Chuỗi gọi API đồng bộ mong manh qua mạng (Temporal Coupling)
def checkout_order(user_id, cart_items, cart_total):
# 1. Gọi sang UserService qua gRPC để trừ tiền ví
try:
user_service.deduct_balance(user_id, cart_total)
except grpc.RpcError as e:
# NẾU TIMEOUT Ở ĐÂY: Liệu ví đã bị trừ tiền chưa?
# Phía UserService có thể đã trừ tiền nhưng mạng bị rớt khi trả kết quả về.
# Chúng ta rơi vào trạng thái không xác định (Unknown State).
raise PaymentFailedException("Không thể xác định trạng thái thanh toán")
# 2. Ghi nhận đơn hàng vào database nội bộ (order_db)
try:
order_db.execute("INSERT INTO orders (user_id, total) VALUES (?, ?)", user_id, cart_total)
except DatabaseError as e:
# DATABASE LỖI HOẶC CRASH GIỮA CHỪNG:
# Ví đã bị trừ tiền thành công ở bước 1, nhưng đơn hàng không được ghi nhận.
# Chúng ta phải cố gắng gọi API hoàn tiền (compensating transaction)
try:
user_service.refund_balance(user_id, cart_total) # Gọi lại qua mạng
except grpc.RpcError:
# API hoàn tiền cũng sập nốt. Thảm họa bất nhất dữ liệu xảy ra!
log.critical(f"Bất nhất dữ liệu nghiêm trọng cho user: {user_id}")
raise OrderCreationException("Lỗi lưu đơn hàng")
Ở môi trường Staging với lượng request nhỏ, phiên bản Microservices chạy hoàn hảo. OrderService gọi sang UserService thông qua HTTP/gRPC để kiểm tra và trừ tiền ví, sau đó tự ghi dữ liệu của mình.
Nhưng khi chiến dịch Flash Sale chính thức bắt đầu ngoài Production, traffic tăng vọt gấp 50 lần. Thảm họa lập tức ập đến.
Do mạng bất ổn và tải hệ thống tăng cao, các cuộc gọi gRPC từ OrderService sang UserService bắt đầu bị timeout liên tục. Một lượng lớn request trừ tiền ví thành công tại UserService nhưng OrderService lại nhận lỗi timeout và quyết định hủy luồng, dẫn đến tình trạng khách hàng bị trừ tiền trong ví nhưng không có đơn hàng nào được tạo.
Để cứu vãn tình thế, các kỹ sư cố gắng đưa vào các thư viện điều phối giao dịch phân tán (Distributed Transaction Coordinator) tự chế, sử dụng các cơ chế khóa (locking) phức tạp qua gRPC. Hệ quả là độ trễ (latency) của một request đặt hàng tăng từ 50ms lên hơn 2000ms. Luồng xử lý bị tắc nghẽn hoàn toàn. Connection pool của cả hai database đều bị vắt kiệt. Hệ thống sập hoàn toàn trong 3 tiếng đồng hồ, gây thiệt hại hàng trăm nghìn đô la doanh thu và để lại một bãi chiến trường dữ liệu rác cần dọn dẹp bằng tay.
3. Tại sao hướng đi "Ngây thơ" lại nghe có vẻ rất thuyết phục?
Tại sao một giải pháp mang lại hậu quả nghiêm trọng như vậy lại được nhiều đội ngũ kỹ sư lựa chọn ngay từ đầu?
Lý do rất đơn giản: Nó cực kỳ trực quan và khớp với tư duy lập trình hướng đối tượng (OOP) và kiến trúc MVC thông thường.
Trong suốt nhiều năm làm việc với Monolith, chúng ta được dạy rằng mỗi bảng trong cơ sở dữ liệu quan hệ đại diện cho một thực thể (Entity). Khi chuyển sang Microservices, tư duy tự nhiên là gán cho mỗi service quản lý một hoặc một nhóm thực thể đó. UserService quản lý bảng users, ProductService quản lý bảng products.
+-------------------------------------------------------------+
| MONOLITH DATABASE |
| +---------------+ +---------------+ +--------------+ |
| | Users | | Orders | | Products | |
| +---------------+ +---------------+ +--------------+ |
+-------------------------------------------------------------+
|
| (Tách "ngây thơ")
v
+------------------+ +------------------+ +------------------+
| UserService | | OrderService | | ProductService |
| +------------+ | | +------------+ | | +------------+ |
| | user_db | | | | order_db | | | |product_db | |
| +------------+ | | +------------+ | | +------------+ |
+------------------+ +------------------+ +------------------+
Cách tiếp cận này hứa hẹn sự rõ ràng:
- Cô lập mã nguồn: Mỗi service nằm ở một repository riêng, nhóm phát triển có thể tự chọn công nghệ và làm việc độc lập.
- Tách biệt schema: Việc thay đổi cấu trúc bảng
userskhông làm ảnh hưởng trực tiếp đến mã nguồn củaOrderServiceở mức biên dịch (compile-time). - Dễ hiểu: Cấu trúc thư mục và cấu trúc database tương ứng 1-1 với các khái niệm nghiệp vụ bề nổi.
Về mặt kỹ thuật, việc tách database lúc này chỉ được coi như một bài toán di chuyển dữ liệu (data migration). Chỉ cần tạo các database instance mới, chạy script migrate và sửa lại mã nguồn ứng dụng để gọi API thay vì chạy câu lệnh JOIN SQL trực tiếp. Nhưng sự mượt mà này chỉ là một ảo ảnh được nuôi dưỡng trong môi trường local – nơi mạng máy tính là hoàn hảo, không có độ trễ và tài nguyên hệ thống gần như vô hạn.
4. Nút thắt thực sự: Tại sao nó vỡ vụn trên Production? (Root Cause)
Sự sụp đổ của cách tiếp cận "ngây thơ" nằm ở chỗ nó bỏ qua một thực tế cốt lõi: Ranh giới vật lý của dữ liệu (bảng quan hệ) không trùng khớp với ranh giới nghiệp vụ (business transaction boundary) của hệ thống.
Trong một ứng dụng Monolith truyền thống, sự nhất quán dữ liệu được bảo đảm bởi cơ chế giao dịch ACID của cơ sở dữ liệu quan hệ. Khi bạn thực hiện một hành động nghiệp vụ phức tạp liên quan đến nhiều bảng (ví dụ: tạo đơn hàng, trừ kho, cập nhật ví người dùng), toàn bộ các câu lệnh SQL được gói gọn trong một Database Transaction duy nhất. Nếu bất kỳ bước nào thất bại, database sẽ tự động rollback toàn bộ các thay đổi về trạng thái ban đầu. Độ trễ của giao dịch này cực kỳ thấp vì tất cả diễn ra trong bộ nhớ và đĩa cứng của một server duy nhất.
Khi bạn tách các bảng này thành user_db, order_db và product_db dựa trên thực thể vật lý, bạn đã phá vỡ hoàn toàn bức tường bảo vệ ACID này. Một giao dịch cục bộ (Local Transaction) giờ đây đã bị biến thành một Giao dịch phân tán (Distributed Transaction) chạy qua môi trường mạng vật lý đầy biến động.
Đây chính là lúc Cái bẫy thực thể (Entity-based splitting trap) sập xuống, để lại những vấn đề chí mạng trên production:
1. Sự bất ổn của môi trường mạng (Fallacies of Distributed Computing)
Mạng máy tính không hề đáng tin cậy. Các lỗi như mất gói tin (packet loss), nghẽn mạng (network congestion) hoặc dịch vụ đích bị crash đột ngột xảy ra liên tục trên production. Khi một cuộc gọi mạng bị timeout, service gọi không thể biết được hành động ở service nhận đã thực sự xảy ra hay chưa. Việc cố gắng tự giải quyết bài toán này bằng các cơ chế retry, hoàn tiền thủ công sẽ nhanh chóng biến mã nguồn thành một mớ hỗn độn (spaghetti code) và cực kỳ dễ sót các trường hợp lỗi ngoài ý muốn (edge cases).
2. Gánh nặng giao dịch phân tán (Distributed Transactions Nightmare)
Để đảm bảo tính nhất quán giữa các database khác nhau, bạn buộc phải sử dụng các pattern phức tạp:
- 2-Phase Commit (2PC): Đòi hỏi một coordinator điều phối và khóa (lock) tài nguyên trên tất cả các database cho đến khi toàn bộ các bên sẵn sàng commit. Cơ chế này làm tăng độ trễ và làm giảm đáng kể thông lượng (throughput) của hệ thống. Chỉ cần một database bị chậm hoặc rớt kết nối, toàn bộ hệ thống sẽ bị kéo chậm theo và có nguy cơ bị khóa chết (deadlock).
- Saga Pattern (Choreography hoặc Orchestration): Yêu cầu bạn thiết kế các giao dịch bù trừ (Compensating Transactions - như API hoàn tiền, cộng lại kho). Việc này làm tăng độ phức tạp của mã nguồn lên gấp nhiều lần, cực kỳ khó viết test và debug khi xảy ra lỗi bất nhất dữ liệu sâu trong luồng xử lý.
3. Sự phụ thuộc liên kết theo thời gian (Temporal Coupling)
Khi tách dịch vụ quá vụn vặt theo bảng, một request từ client đòi hỏi chuỗi gọi đồng bộ (synchronous calls) liên tục qua nhiều service khác nhau. Chỉ cần một service trong chuỗi gặp sự cố hoặc phản hồi chậm, toàn bộ request của khách hàng sẽ bị timeout hoặc thất bại. Lợi thế về tính sẵn sàng cao (High Availability) của Microservices hoàn toàn biến mất, thay vào đó bạn nhận được một hệ thống dễ sập dây chuyền (Cascading Failure) và chậm chạp hơn cả Monolith.
5. Tư duy mới: Hãy tách theo Bounded Context, không tách theo Table
Để thoát khỏi cái bẫy này, chúng ta cần thay đổi hoàn toàn lăng kính thiết kế. Thay vì nhìn hệ thống dưới dạng các thực thể dữ liệu vật lý (bảng và cột), hãy nhìn hệ thống qua lăng kính của Domain-Driven Design (DDD).
Trong DDD, có hai khái niệm cốt lõi quyết định cách chúng ta phân chia ranh giới dịch vụ và cơ sở dữ liệu:
1. Bounded Context (Bối cảnh giới hạn)
Một mô hình domain không bao giờ mang cùng một ý nghĩa trong toàn bộ hệ thống doanh nghiệp. Thực thể "User" ở bộ phận Tài khoản (Identity) chỉ quan tâm đến email, password_hash, status. Nhưng đối với bộ phận Bán hàng (Sales/Ordering), "User" thực chất là một "Customer" với các thông tin như địa chỉ giao hàng, lịch sử mua sắm và hạn mức tín dụng.
Thay vì tạo ra một UserService duy nhất quản lý một bảng users khổng lồ phục vụ mọi nơi, chúng ta nên định nghĩa các Bounded Context riêng biệt:
- Identity Context: Chịu trách nhiệm về đăng ký, đăng nhập và xác thực.
- Sales Context: Chịu trách nhiệm về giỏ hàng, đơn hàng và quy trình thanh toán.
Trong Sales Context, chúng ta hoàn toàn có thể lưu trữ một bản sao thông tin khách hàng (chỉ gồm các trường cần thiết cho việc đặt hàng như customer_id, shipping_address, customer_status) ngay trong cơ sở dữ liệu của Sales. Khi thông tin khách hàng thay đổi ở Identity Context, một sự kiện bất đồng bộ (Asynchronous Event) sẽ được gửi đi để cập nhật lại bản sao này.
2. Aggregate (Cụm thực thể)
Một Aggregate là một tập hợp các đối tượng domain có liên quan chặt chẽ với nhau và được coi như một đơn vị duy nhất khi thay đổi dữ liệu. Điểm quan trọng nhất: Aggregate là ranh giới của sự nhất quán giao dịch (Boundary of Transactional Consistency).
Mọi thay đổi dữ liệu bên trong một Aggregate phải được thực hiện thông qua một transaction duy nhất để đảm bảo tính toàn vẹn của các luật nghiệp vụ (Business Invariants).
Ví dụ: Một Order và các OrderItems của nó thuộc cùng một Aggregate. Bạn không thể có một dòng OrderItem tồn tại mà không thuộc về một Order nào, và tổng tiền của Order phải luôn bằng tổng tiền của các OrderItems. Do đó, Order và OrderItem bắt buộc phải nằm chung một cơ sở dữ liệu để có thể cập nhật đồng thời qua một transaction cục bộ (Local Transaction).
Ngược lại, Inventory (Tồn kho) và Order không nhất thiết phải nằm chung một Aggregate. Việc trừ kho khi tạo đơn hàng có thể được xử lý bất đồng bộ hoặc chấp nhận nhất quán cuối cùng (Eventual Consistency). Nếu sản phẩm hết hàng sau khi đơn hàng được tạo, hệ thống có thể chuyển trạng thái đơn hàng thành "Hủy do hết hàng" và thực hiện hoàn tiền tự động.
Hãy cùng so sánh hai cách tiếp cận qua sơ đồ dưới đây:
Cách tiếp cận 1: Tách theo Table (Entity-based - SAI)
+------------------+ +------------------+
| UserService | | OrderService |
| +------------+ | | +------------+ |
| | users | | <-------> | | orders | |
| +------------+ | (gRPC) | +------------+ |
+------------------+ +------------------+
^ ^
| |
(Phải JOIN dữ liệu hoặc gọi API đồng bộ liên tục)
Cách tiếp cận 2: Tách theo Bounded Context (Domain-driven - ĐÚNG)
+----------------------------+ +-----------------------------+
| Identity Bounded Context | | Sales Bounded Context |
| +---------------------+ | | +---------------------+ |
| | UserService | | | | OrderService | |
| | - users (auth) | | | | - orders | |
| +---------------------+ | | | - customers (copy) | |
| +---------------------+ | | +---------------------+ |
| | identity_db | | | | sales_db | |
| +---------------------+ | | +---------------------+ |
+----------------------------+ +-----------------------------+
| ^
+---------[ Event: UserCreated ]------+
(Bất đồng bộ)
Bằng cách đưa thông tin cần thiết của khách hàng vào Bounded Context của Sales, OrderService giờ đây có thể tạo đơn hàng hoàn toàn độc lập mà không cần thực hiện bất kỳ cuộc gọi mạng đồng bộ nào sang UserService. Sự nhất quán giao dịch được thu hẹp về mức cục bộ trong sales_db, loại bỏ hoàn toàn gánh nặng của các giao dịch phân tán.
6. Chiến lược di chuyển an toàn bằng phương pháp Split-Phase
Nếu bạn đang sở hữu một hệ thống Monolith với một cơ sở dữ liệu dùng chung và muốn chuyển đổi sang Microservices, việc ngay lập tức tách database vật lý là một quyết định cực kỳ mạo hiểm. Hãy áp dụng chiến lược di chuyển từng bước (Split-Phase Migration) để giảm thiểu rủi ro.
[ Monolith DB ]
|
v (Phase 1: Tách Schema logic - Chung DB vật lý)
[ Schema Identity | Schema Sales ]
|
v (Phase 2: Xóa JOIN trực tiếp, chuyển sang gọi API/Event)
[ Code Service A ] ----(API)----> [ Code Service B ]
| |
[ Schema Identity ] [ Schema Sales ]
|
v (Phase 3: Tách Database vật lý)
[ identity_db ] [ sales_db ]
Bước 1: Phân tách Schema logic (Shared Database, Separate Schemas)
Đừng vội mua thêm server hay tạo instance database mới. Bước đầu tiên là nhóm các bảng có liên quan chặt chẽ về mặt nghiệp vụ vào các Schema logic khác nhau ngay trên chính cơ sở dữ liệu hiện tại (ví dụ: trong PostgreSQL, bạn tạo schema identity và schema sales).
Đồng thời, thiết lập một quy tắc nghiêm ngặt trong mã nguồn: Dịch vụ quản lý Schema A tuyệt đối không được phép thực hiện câu lệnh query (SELECT, INSERT, UPDATE) trực tiếp vào các bảng thuộc Schema B.
- Tại sao làm vậy: Bước này giúp bạn định hình lại cấu trúc code và ranh giới dữ liệu mà không phải chịu bất kỳ chi phí nào về độ trễ mạng hay rủi ro vận hành database mới. Nếu phát hiện ranh giới chia bị sai (ví dụ: nhận ra hai bảng vẫn cần JOIN liên tục với nhau), bạn chỉ cần cấu hình lại schema hoặc gộp bảng rất dễ dàng mà không cần rollback dữ liệu phức tạp.
Bước 2: Loại bỏ liên kết trực tiếp (Decouple Code & Eliminate Joins)
Sau khi đã phân tách schema logic, hãy tiến hành viết lại toàn bộ các câu lệnh SQL JOIN chéo giữa các schema.
- Thay vì chạy câu lệnh
JOIN identity.users ON sales.orders.user_id = identity.users.id, hãy chuyển sang cơ chế Application-level Join (gọi API lấy danh sách user trước, sau đó map vào đơn hàng trong code) hoặc tốt hơn là áp dụng Data Redundancy (lưu trữ trực tiếp tên khách hàng vào bảng đơn hàng tại thời điểm đặt hàng). - Đây cũng là giai đoạn bạn đưa vào các Message Broker (như Kafka hoặc RabbitMQ) để đồng bộ hóa dữ liệu bất đồng bộ giữa các schema thông qua các sự kiện (Events), thay thế dần cho các luồng xử lý đồng bộ.
Bước 3: Phân tách cơ sở dữ liệu vật lý (Physical Separation)
Chỉ khi toàn bộ mã nguồn đã hoàn toàn độc lập, không còn bất kỳ câu lệnh query chéo hay giao dịch trực tiếp nào giữa các schema logic, bạn mới tiến hành tách chúng thành các database instance vật lý riêng biệt.
Bạn có thể thực hiện việc này một cách an toàn bằng cách sử dụng các công cụ CDC (Change Data Capture) như Debezium hoặc cơ chế Replication của database để sao chép dữ liệu từ schema cũ sang database mới theo thời gian thực. Sau đó, tiến hành cấu hình lại kết nối (Connection String) của dịch vụ và tắt schema cũ mà không gây downtime cho hệ thống.
7. Sự đánh đổi: Chúng ta được gì và mất gì?
Kiến trúc phần mềm là nghệ thuật của sự đánh đổi. Khi bạn quyết định đi theo mô hình Database-per-Service (ngay cả khi đã chia chuẩn theo Bounded Context), bạn phải chấp nhận những mất mát để đổi lấy lợi ích.
Những gì bạn nhận được (Benefits):
- Khả năng mở rộng độc lập (Independent Scaling): Bạn có thể scale riêng database của Catalog (đọc rất nhiều) bằng cách thêm các Read Replica mà không cần tăng tài nguyên cho database của Billing (ghi nhiều, đọc ít).
- Cô lập lỗi (Fault Isolation): Nếu database của dịch vụ Khuyến mãi (Promotion) bị sập do truy vấn nặng, người dùng vẫn có thể duyệt sản phẩm và thanh toán đơn hàng bình thường.
- Quyền tự chủ về công nghệ (Polyglot Persistence): Dịch vụ tìm kiếm (Search Service) can dùng Elasticsearch, dịch vụ giỏ hàng (Cart Service) dùng Redis, trong khi dịch vụ đơn hàng (Order Service) dùng PostgreSQL. Mỗi đội ngũ tự do lựa chọn công cụ tốt nhất cho bài toán của họ.
Những gì bạn phải trả giá (Costs & Risks):
- Mất tính toàn vẹn tham chiếu (No Foreign Keys): Database không còn khả năng tự kiểm tra xem
user_idtrong bảngorderscó thực sự tồn tại trong bảngusershay không. Bạn phải tự gánh vác việc kiểm tra tính hợp lệ này ở tầng ứng dụng. - Độ phức tạp khi truy vấn (Complex Queries): Bạn không thể viết một câu lệnh SQL JOIN đơn giản để lấy ra "danh sách đơn hàng kèm theo tên và email của người mua". Bạn buộc phải xây dựng các dịch vụ tổng hợp dữ liệu, áp dụng pattern CQRS (Command Query Responsibility Segregation) hoặc sử dụng các công cụ tìm kiếm tập trung như Elasticsearch để tạo các Read Models.
- Chấp nhận sự nhất quán cuối cùng (Eventual Consistency): Bạn phải làm quen và huấn luyện người dùng cũng như các phòng ban nghiệp vụ chấp nhận thực tế rằng dữ liệu không phải lúc nào cũng được cập nhật ngay lập tức ở mọi nơi. Báo cáo tài chính có thể sẽ có độ trễ vài phút so với giao dịch thực tế.
8. Khi nào thiết kế này sẽ thất bại? (Failure Cases)
Ngay cả khi bạn đã hiểu rõ lý thuyết về Bounded Context và Aggregate, thiết kế Database-per-Service vẫn có thể dẫn đến thất bại trong các trường hợp sau:
- Tách quá vụn vặt (Nanoservices Trap): Đội ngũ cố gắng tách database cho mọi thực thể nhỏ nhặt nhất. Ví dụ: Tách bảng
users,user_addresses,user_profilesthành 3 dịch vụ khác nhau với 3 database riêng biệt. Việc này tạo ra một "mạng lưới nhện" các cuộc gọi API gRPC chồng chéo chỉ để lấy thông tin cơ bản của một người dùng, làm tê liệt hiệu năng của toàn bộ hệ thống. - Áp dụng quá sớm (Premature Splitting): Đối với các dự án startup hoặc sản phẩm mới khởi nghiệp, nơi mô hình nghiệp vụ (Business Model) thay đổi liên tục hàng tuần. Việc tách database quá sớm sẽ đóng băng cấu trúc dữ liệu, khiến mỗi lần thay đổi tính năng là một lần cực hình vì phải migrate dữ liệu và sửa đổi API contract của nhiều service cùng lúc. Trong giai đoạn này, một thiết kế Monolith sạch với một database duy nhất luôn là lựa chọn tối ưu hơn.
- Hạ tầng giám sát nghèo nàn: Thiếu cơ chế Distributed Tracing (truy vết phân tán) và Centralized Logging. Khi dữ liệu bị bất nhất giữa các database, bạn sẽ hoàn toàn mù mịt không biết lỗi xảy ra từ đâu, do sự kiện nào bị mất hay do service nào xử lý sai.
9. Tổng kết và Tư duy Kỹ sư (Key Takeaways)
- Ranh giới nghiệp vụ quyết định ranh giới dữ liệu: Tuyệt đối không chia service theo cấu trúc các bảng vật lý (Entity-based splitting). Hãy chia dữ liệu dựa trên Bounded Context và Aggregate của Domain-Driven Design.
- ACID là tài sản quý giá, hãy trân trọng nó: Đừng vứt bỏ khả năng giao dịch cục bộ (Local Transaction) của database quan hệ trừ khi bạn bắt buộc phải scale độc lập. Nếu hai thực thể luôn cần nhất quán dữ liệu ngay lập tức, chúng nên nằm chung một database.
- Di chuyển là một hành trình, không phải một cú nhảy: Hãy luôn bắt đầu bằng việc tách Schema logic (Shared Database) để kiểm chứng ranh giới trước khi tiến hành tách Database vật lý hoàn toàn.
10. Lời kết và Open Loop
Tách cơ sở dữ liệu thành công mới chỉ là bước khởi đầu để bạn cô lập các dịch vụ. Thách thức tiếp theo nằm ở việc: Khi các cơ sở dữ liệu đã nằm riêng biệt, làm thế nào để các dịch vụ này giao tiếp với nhau một cách hiệu quả, an toàn và giữ được cam kết (API Contracts)?
Chúng nên sử dụng giao thức truyền thống như REST, chuyển sang hiệu năng cao với gRPC hay sự linh hoạt của GraphQL? Có một lựa chọn nào là "tốt nhất" cho mọi trường hợp, hay tất cả đều là những sự đánh đổi đầy toan tính?
Chúng ta sẽ cùng nhau bóc tách và tìm câu trả lời trong bài viết tiếp theo: Episode 04: REST, gRPC hay GraphQL? Không có lựa chọn tốt nhất.
11. Tách theo database là anti-pattern vì nó tối ưu theo storage, không theo business
Khi một team nhìn schema trước rồi chia service theo bảng dữ liệu, họ thường vô tình đẩy kiến trúc đi theo cấu trúc lưu trữ thay vì theo domain behavior. Điều đó tạo ra một vấn đề rất âm thầm: business flow không còn nằm trọn trong một service boundary nữa.
Hậu quả là những thao tác tưởng như đơn giản ở nghiệp vụ, ví dụ tạo đơn hàng, áp mã giảm giá, giữ tồn kho, xác nhận thanh toán, lại phải chạy qua quá nhiều service chỉ vì mỗi service "ôm" một nhóm bảng. Team không thiết kế xung quanh capability của hệ thống, mà thiết kế xung quanh sơ đồ ERD.
Đây là lý do anti-pattern này đặc biệt nguy hiểm. Nó khiến hệ thống trông có vẻ đã tách service, nhưng thực chất mọi use case đều trở thành distributed transaction trá hình.
12. Một boundary tốt nên được nhận ra bằng câu hỏi nào?
Boundary service tốt thường trả lời được ba câu hỏi:
- service này sở hữu quyết định nghiệp vụ nào?
- nếu business rule của miền này đổi, team nào phải chịu trách nhiệm chính?
- service khác có thật sự cần biết dữ liệu nội bộ của nó, hay chỉ cần outcome / contract ở mức business?
Nếu không trả lời được ba câu đó mà vẫn tách theo bảng, team sẽ sớm rơi vào cảnh service gọi chéo dày đặc, deploy chéo liên tục và không ai còn chắc hệ thống thực sự "owned" bởi ai nữa.
Microservices không bắt đầu từ chuyện chia dữ liệu. Nó bắt đầu từ chuyện chia trách nhiệm. Database chỉ nên đi theo boundary đó, không nên dẫn dắt boundary đó.
🔍 Đi sâu hơn cùng TechCraft
Microservices chỉ thực sự phát huy giá trị khi bạn hiểu rõ system boundary, communication pattern, failure mode và các trade-off vận hành phía sau nó.
Nếu bạn muốn tiếp tục rèn tư duy thiết kế hệ thống theo hướng thực chiến hơn, Dev Insider là nơi TechCraft tập trung các series đào sâu vào backend, database, distributed systems và production thinking.
🚀 Dev Insider https://www.patreon.com/techcraft_official/posts/vi-sao-dev-ra-161163881?collection=2220113
📘 Facebook https://www.facebook.com/techcraft.official
🎥 YouTube https://www.youtube.com/@techcraft.official
🎵 TikTok https://www.tiktok.com/@techcraft.official
Hiểu trade-off. Thiết kế tốt hơn.
All rights reserved