I. Microservices là gì mà khiến giới giang hồ điên đảo vì nó suốt nhiều năm???
Moshi Moshi, xin chào anh em, mình là NekoArcoder đây ạ, đây là bài đầu tiên trong chuỗi series "Bạn mù tịt về Microservices, tôi cũng thế". Hi vọng mình sẽ cook đủ đơn giản để truyền tải tới cho mọi người cách nhìn dễ hiểu nhất về Microservices. Hẹ hẹ anh em mình cứ thế thôi, Lẹt go nào.
1. Monolithic là một Hurricane Green Robot???:
Để trả lời cho câu hỏi này, chúng ta cần phải xem lại kiến trúc Monolithic mà thời sinh viên chúng ta đã làm việc với nó hàng ngày trước.
Tất cả mã nguồn cần thiết để xây dựng ứng dụng của chúng ta đều được gói gọn trong một codebase duy nhất, và chúng ta triển khai toàn bộ codebase đó như một khối thống nhất.
Minh họa cho một kiến trúc Monolithic khi một request được gửi tới như sau:
- Đầu tiên chúng ta nhận được một yêu cầu (request) từ trình duyệt hoặc thiết bị di động của người dùng.
- Yêu cầu đó sẽ đi vào ứng dụng, có thể trải qua một số lớp middleware tiền xử lý, sau đó đi đến một router.
- Router này sẽ phân tích request và quyết định chuyển nó đến một tính năng cụ thể để xử lý sâu hơn.
Ví dụ, request được chuyển đến tính năng A. Tại đây, hệ thống có thể sẽ đọc/ghi dữ liệu từ cơ sở dữ liệu, sau đó xây dựng một phản hồi và trả về cho người dùng.
Nếu phải mô tả một kiến trúc monolith, chúng ta có thể tóm tắt như sau:
Monolithic chứa tất cả phần định tuyến (routing), các middleware, logic nghiệp vụ (business logic), các config và code truy cập cơ sở dữ liệu để hiện thực tất cả các tính năng của ứng dụng. Chúng tạo nên một thể thống nhất, chặt chẽ, khó có thể tách rời.
Hơi lý thuyết rồi, mọi người cứ tưởng tượng Monolithic giống như là một con Robot của siêu nhân xanh trong Siêu nhân cuồng phong vậy.

Đây là một con Robot nguyên khối, các bộ phận tay, chân, đầu đều gắn liền với nhau không thể tách rời.
Vẫn hoạt động ngon lành, mạnh, nhanh, do nó chỉ được điều khiển bởi một thằng siêu nhân thôi, nên khi có một yêu cầu đánh đấm thì chỉ cần ra lệnh, thì mệnh lệnh đó được truyền trực tiếp đến bộ phận đó xử lý và thực hiện thao tác ngay, không cần bước trung gian gì cả.
Ưu điểm:
- Nhạc thì thấm, đấm thì đau, nhỏ nhỏ mà có võ, đấm vỡ alo các loại quái từ nhỏ tới trung bình, phù hợp với đại đa số nhu cầu (Tức là nếu bạn là doanh nghiệp nhỏ, CCU chỉ ở mức nhỏ đến trung bình thì Monolithic là quá đủ).
- Vì là một chiếc trực thăng biến ra nên nó khá nhỏ, dễ bảo trì và gắn thêm các đồ chơi cho nó (Khi là một khối thì việc bảo trì và phát triển thêm tính năng cũng rất rất đơn giản, miễn là có doc đủ rõ ràng và clean code là được).
- Chi phí phát triển và tu dưỡng thấp. (Nhỏ nhỏ thì chi phí phát triển và bảo trì sẽ không cao rùi).
Nhược điểm:
- Khi trùm cuối xuất hiện thì sức mạnh một mình đã không còn đủ nữa, lúc này bạn chỉ mong mình là Naruto để thực hiện thông não chi thuật con Boss (Khi số lượng CCU hoặc độ lớn của dự án cực kỳ khủng thì Monolithic đã không còn phù hợp nữa - Instagram là một ngoại lệ nhé.)
- Khi một bộ phận như tay, chân bị hỏng thì Robot gần như bị loại khỏi cuộc chơi, không thể tháo lắp bộ phận do nó là một khối thống nhất, chỉ có thể đem vào lại bảo trì thui. (Ở các dự án yêu cầu downtime cực kỳ thấp thì việc này không thể chấp nhận được.)
- Sự cồng kềnh khi muốn Robot mạnh hơn thì phải độ thêm nhiều đồ chơi hơn hoặc phải tự nâng cấp động cơ, dẫn đến sự độ chế phá vỡ kỹ thuật, về lâu về dài sẽ trở thành một con Robot Ục ịch trì trệ (Tương tự như vậy, khi việc nhân sự luôn thay đổi trong một dự án luôn là điều bình thường ở các công ty, nên sẽ luôn có sự chồng chéo logic hoặc tự "độ chế" code dẫn tới một đống bùi nhùi.)
2. Microservice là một Robot được kết hợp từ các bộ phận khác nhau:
Bây giờ, chúng ta sẽ dựa vào sơ đồ ở phần 1 và chỉ cần thay đổi một vài điểm để mô tả để hiểu microservices là gì.
Chúng ta định nghĩa microservice như sau:
Một microservices về bản chất sẽ chứa chính các Monolothic đơn lẻ đã bao gồm các routing, middleware, logic nghiệp vụ và code truy cập cơ sở dữ liệu cần thiết để hiện thực một tính năng duy nhất của ứng dụng.
Đây chính là sự khác biệt cốt lõi:
- Monolith chứa tất cả mã nguồn cần thiết để hiện thực mọi tính năng của ứng dụng.
- Một Microservice đơn lẻ thì chỉ chứa mã nguồn cần thiết cho một tính năng duy nhất.
Trong kiến trúc microservices, chúng ta sẽ tách từng tính năng riêng biệt và đóng gói chúng vào các service độc lập. Ví dụ qua hình sau:
Ví dụ: Service A sẽ chứa toàn bộ mã nguồn cần thiết để tính năng A hoạt động. Nó có middleware riêng, router riêng, và thậm chí là cơ sở dữ liệu riêng.
Điểm cực kỳ quan trọng bạn luôn phải nhớ đó chính là:
Mỗi service là hoàn toàn độc lập, tự chứa (self-contained) code của chính nó để thực hiện một tính năng duy nhất.
Cứ tưởng tượng dự án Microservices của bạn là một con Robot của 5 thằng siêu nhân gộp lại thành một con Robot tổng vậy, mỗi một con sẽ đóng vai trò như đầu, thân, tay, chân,...
Mỗi con đều có thể tự hoạt động độc lập, không phụ thuộc vào nhau, chiến đấu một cách riêng lẻ.
Khi chúng kết hợp lại thì sẽ tạo thành một khối thống nhất nhưng vẫn đạt được trạng thái Loose coupling. Có thể dễ dàng thay thế một bộ phận bởi một một bộ phận khác khi bị hỏng ngay trên chiến trường mà không phải đem cả khối Robot lớn đi bảo trì.
Khi muốn thêm một bộ phận mới thì cũng không ảnh hưởng các bộ phận cũ, từ đó chiến đấu mượt mà hơn mà không có độ trễ.
Vậy tổng kết lại, chúng ta hiểu kiến trúc Microservices như sau:
Một microservice đơn lẻ sẽ chứa toàn bộ mã nguồn cần thiết để giúp một tính năng của ứng dụng hoạt động.
II. Kho lương của kiến trúc Microservices (Data in microservices)
Khi chúng ta đã hiểu được hòm hòm Microservices là gì thì gặp ngay một con boss khó nhằn trong hành trình tìm hiểu, bạn có thể đoán ra không?
Thử quay lại một chút nhé, khi nhìn vào lại sơ đồ này và mình sẽ gợi ý một chút:
Yes, đúng vậy. Công nghệ ở mỗi feature có thể khác nhau, logic nghiệp vụ có thể khác nhau, nhưng database cũng phải khác nhau nốt luôn?
Bạn nhận ra sự nghiêm trọng của nó chứ? Khi thiết kế cơ sở dữ liệu quan hệ (SQL), bạn cần đảm bảo các mối quan hệ giữa các bảng được thể hiện một cách chặt chẽ thông qua việc sử dụng các khóa ngoại (foreign keys).
Thế còn bây giờ, ở trong kiến trúc Microservices, ở mỗi service lại có một database riêng (Nếu cần) để đạt được trạng thái cô lập tuyệt đối, không phụ thuộc vào nhau, như vậy thì chúng ta phải làm thế nào?
Vậy chúng ta đang có ở đây là 2 vấn đề cần phải nắm rõ khi "Quản lý dữ liệu giữa các service":
- Cách chúng ta lưu trữ dữ liệu bên trong một service sẽ như thế nào?
- Cách chúng ta giao tiếp, chia sẻ dữ liệu giữa các service khác nhau sẽ ra sao?
Đây là một vấn đề lớn, rất khó, và là rào cản chính khi triển khai microservices hiệu quả. Bạn sẽ cảm thấy nó khó vì trong kiến trúc microservices, cách lưu trữ và truy cập dữ liệu khác so với cách bạn từng quen trong ứng dụng monolithic.
Nhưng không sao, đã có tôi ở đây với bạn, chúng ta cứ tiếp tục từ từ tìm hiểu nào!
1. Cách lưu trữ dữ liệu trong microservices
Như mình đã trình bày, mỗi service có cơ sở dữ liệu riêng biệt và không phụ thuộc vào nhau. Ta cũng có thể phát biểu như sau:
Với microservices, mỗi service sẽ có database riêng của nó, nếu nó cần lưu trữ dữ liệu.
- Nếu một service không cần dữ liệu, thì không cần cấp database cho nó.
- Nhưng nếu cần, không có chuyện dùng chung database. Mỗi service sẽ có database độc lập.
Không được xài chung như thế này nhé, mỗi nhà một chồng một vợ, chứ không có chuyện léng phéng sang nhà hàng xóm nhé 😂
2. Cách truy cập dữ liệu (và điều chúng ta không nên làm)
Không bao giờ một service này được phép truy cập trực tiếp vào database của service khác.
Chỉ đơn giản thế thôi, ví dụ nhé: Service A không bao giờ được phép “chọc” vào database của Service B, trong bất kỳ tình huống nào. Dù là chị Database B có kẹt trong máy giặt hay nhờ bạn qua sửa ống nước đi chăng nữa cũng KHÔNG được nhé =))).
Túm cái váy lại, chỉ cần bạn nhớ 2 thứ:
1. Mỗi service có database riêng của nó (nếu cần).
2. Một service không bao giờ truy cập database của service khác.
3. Tại sao tôi phải làm như vậy?
Đúng vậy, chỉ Nam chỉ Bắc mà chả nói tại sao thì sao mà hiểu được đúng không, thực ra có tầm 4 lý do mà mình có thể nghĩ ra cho điều này.
Mỗi service nên hoạt động độc lập
Pattern này có tên chính thức là “Database per service”, tức là mỗi service có database riêng.
Giả sử bạn có tất cả service cùng dùng chung một database. Nếu database đó gặp trục trặc, toàn bộ hệ thống sẽ sập. Không chỉ vậy, việc scale (mở rộng) database chung này sẽ rất khó khăn.
Bạn sẽ phải scale một database duy nhất để phục vụ mọi service, thay vì chỉ scale những phần thực sự cần.
Tránh phụ thuộc chéo giữa các service
Lấy ví dụ nhé:
Nếu Service A truy cập database của Service B, và database B bị lỗi, thì Service A cũng sẽ sập theo.
Điều này tạo ra sự phụ thuộc chéo nguy hiểm, khiến một lỗi nhỏ có thể lan rộng toàn hệ thống.
Tránh lỗi do thay đổi schema
Chúng ta sẽ nhập vai là Dev cho một công ty thuộc ngành công nghiệp không khói của Nhật để cho dễ hiểu nhé =)))))
- Giả sử Service CODE gọi trực tiếp database của Service ACTOR để lấy thông tin của diễn viên.
- Ban đầu, dữ liệu trả về có dạng {"name": "Himari"}.
- Rồi team Service ACTOR quyết định đổi "name" thành "url" mà không thông báo.
- Lần tới Service CODE gọi, nó nhận key "url", mà lại đang mong đợi "name", thế là lỗi xảy ra.
Lỗi này rất khó debug, vì nó không đến từ service A, cũng không phải lỗi hệ thống... mà là do sự thay đổi không được đồng bộ giữa các team.
Tối ưu hiệu năng bằng database chuyên biệt
Một số service có thể hoạt động hiệu quả hơn khi dùng loại database khác.
Ví dụ:
- Có service nên dùng MongoDB thay vì PostgreSQL.
- Hoặc Redis thay vì MySQL.
Khi dùng “database per service”, bạn có quyền chọn loại database phù hợp nhất cho từng service.
4. Thế ngoài kia họ đang làm thế nào?
Bạn có thể đang thắc mắc là thực tế ngoài kia họ có làm như vậy hay không, chúng ta có thực sự phải áp dụng pattern này không?
Thì câu trả lời của mình là có và họ đang thực sự áp dụng, mình xin trích lại câu của anh Stephen Grider như sau:
Tất cả các đội kỹ thuật giỏi đang triển khai microservices đều áp dụng đúng pattern này – mỗi service một database.
Xem ra chúng ta đã đi đúng hướng rồi, nhưng nếu vấn đề chỉ đơn giản thế thì làm sao gọi là một Big proplem đúng không =))))
III. Nói thì dễ - Làm mới khó
Ở ví dụ này thì mình xin phép mượn ý tưởng E-Commerce của anh Stephen Grider để trình bày luôn, tại mình lười vẽ hình á =))).
Ví dụ: Ứng dụng thương mại điện tử đơn giản
Hãy nhìn vào sơ đồ sau: Ứng dụng này chỉ có 3 chức năng rất đơn giản:
- Cho phép người dùng đăng ký tài khoản (sign up).
- Hiển thị danh sách sản phẩm như mũ, quần, giày.
- Cho phép người dùng mua sản phẩm.
Trường hợp 1: Kiến trúc Monolithic
Hãy tưởng tượng rằng tôi và bạn đang xây dựng ứng dụng này theo mô hình monolithic quen thuộc.
- Chúng ta sẽ có thể tạo các thư mục như model, repository, cotroller, service,... Nhưng tựu chung lại thì chúng ta vẫn sẽ viết hết vào một thư mục tổng đúng chứ?
- Sau đó chúng ta sẽ chọn một loại CSDL nào đó như MySQL, MongoDB,... rồi để app của chúng ta kết nối đến CSDL này.
- Sau đó, trong CSDL này chúng ta sẽ tạo ra 3 bảng (hoặc collection): users (Để triển khai tính năng đăng ký), products (Để triển khai tính năng hiển thị sản phẩm) và orders (Để triển khai tính năng mua sản phẩm)
Giả sử giờ chúng ta muốn thêm một tính năng mới:
Hiển thị danh sách sản phẩm mà một người dùng cụ thể đã mua.
Trong mô hình monolithic, chúng ta sẽ triển khai tính năng cực kỳ dễ dàng như sau:
- Truy vấn bảng users để kiểm tra user có tồn tại không
- Truy vấn bảng orders để tìm các đơn hàng do người đó tạo
- Truy vấn bảng products để lấy chi tiết các sản phẩm đã được mua
Tất cả chỉ là các truy vấn trong cùng một cơ sở dữ liệu, rất đơn giản, rất hiệu quả.
Trường hợp 2: Kiến trúc Microservices
Giờ ta thử chuyển đổi ứng dụng này sang mô hình microservices.
Chúng ta sẽ chia ứng dụng thành 3 service:
- Service User: xử lý đăng ký người dùng
- Service Product: quản lý danh sách sản phẩm
- Service Order: xử lý việc tạo đơn hàng (mua sản phẩm)
Mỗi service có database riêng của nó.
Vấn đề phát sinh
Giả sử bây giờ ta muốn thêm một Service ListOrderedProducts, có nhiệm vụ:
Hiển thị danh sách sản phẩm mà một người dùng cụ thể đã đặt mua.
Nếu ta nghĩ như cách trong monolithic, chúng ta có thể làm như sau:
- Service ListOrderedProducts sẽ gọi đến database của Service User để lấy thông tin user.
- Sau đó gọi đến database của Service Order để lấy đơn hàng.
- Cuối cùng gọi đến database của Service Product để lấy chi tiết sản phẩm.
Nhưng!!! chúng ta đã nói gì nào?
Trong kiến trúc microservices, một service không được phép truy cập trực tiếp vào database của service khác.
Vậy câu hỏi đặt ra là:
Làm sao chúng ta có thể xây dựng được Service ListOrderedProducts mà không được truy cập vào dữ liệu của các service khác?
Và đó chính là Big Problem
Đây là cốt lõi của bài toán quản lý dữ liệu trong microservices:
Chúng ta muốn mở rộng hoặc thay đổi ứng dụng. Nhưng lại không được truy cập trực tiếp dữ liệu từ service khác. Vậy thì làm sao lấy được dữ liệu cần thiết?
Đó chính là lý do vì sao quản lý dữ liệu giữa các service là rất khó. Ngoài ra còn rất nhiều vấn đề khác nữa, nhưng đây là vấn đề đầu tiên và khó nhai khi bạn tiếp cận về kiến trúc microservices.
Do you give up? No... No... Nothing can beats a jet 2 holidays =))) Khó khó mới hay chứ đúng không ạ, mình sẽ nói tiếp cách hóa giải chiêu 2 Điêu Thuyền ở mục tiếp theo hẹ hẹ.
IV. Trời sinh Du (Sync) sao còn sinh Lượng (Async)
Ở mục trước, chúng ta đã xem qua một ứng dụng thương mại điện tử đơn giản, và thấy rằng nếu ta thêm một service mới cần phụ thuộc vào dữ liệu từ các service khác, mọi thứ bắt đầu trở nên rắc rối một cách nhanh chóng khi mà tuân theo mô hình "Database per service" bởi vì service ListOrderedProducts không thể trực tiếp truy cập dữ liệu từ database của các service khác.
Vì vậy trong phần này mình sẽ có 2 sách lược để anh em thi triển và giải quyết vấn đề Chubby như Tr... à mà thôi. Hai sách lược này chủ yếu xoay quanh cách các service giao tiếp với nhau. Và đó là:
1. Synchronous Communication (Giao tiếp đồng bộ)
Trong mô hình giao tiếp đồng bộ, một service gửi yêu cầu trực tiếp đến một service khác và chờ phản hồi trước khi tiếp tục xử lý. Ví dụ, Service A gọi Service B để lấy dữ liệu, và chỉ khi nhận được kết quả từ Service B thì Service A mới tiếp tục gọi Service C. Quá trình này diễn ra theo chuỗi tuần tự, trong đó mỗi bước đều phụ thuộc vào kết quả của bước trước, đó chính là đặc điểm của Synchronous Communication.
Lấy lại ví dụ ứng dụng thương mại điện tử hồi nãy, chúng ta có bốn service User, Product, và Order, ListOrderedProducts.
Giả sử người dùng gửi yêu cầu đến service ListOrderedProducts với nội dung: “Hiển thị tất cả sản phẩm mà người dùng số 1 đã đặt hàng.” Chúng ta sẽ có các bước như sau:
- Bước 1: Service ListOrderedProducts gửi yêu cầu tới Service User để kiểm tra xem user đó có tồn tại không.
- Bước 2: Nếu user tồn tại, ListOrderedProducts gửi tiếp yêu cầu đến Service Order để lấy danh sách đơn hàng của người dùng đó.
- Bước 3: Từ danh sách đơn hàng, ListOrderedProducts gọi Service Product để lấy thông tin chi tiết về sản phẩm.
Cuối cùng sau 3 bước chờ đợi, Service ListOrderedProducts đã có đủ dữ liệu để trả kết quả cho người dùng.
Trong suốt quá trình này, Service ListOrderedProducts không hề truy cập trực tiếp vào database của bất kỳ service nào khác.
Chu Du thực sự rất giỏi (Ưu điểm)
- Mô hình đồng bộ rất dễ hiểu, dễ hình dung.
- Service ListOrderedProducts không cần có database riêng, vì nó chỉ phụ thuộc dữ liệu từ các service khác.
Nhưng không phải là toàn năng (Nhược điểm)
- Nếu một trong các service phụ thuộc (ví dụ User) bị sập, thì ListOrderedProducts cũng không hoạt động được.
- Chỉ cần một request giữa các service thất bại, toàn bộ luồng xử lý bị lỗi.
- Tốc độ xử lý phụ thuộc vào service chậm nhất. Ví dụ: nếu request đến User mất 10ms, đến Order mất 10ms, nhưng đến Product mất 20 giây thì tổng thời gian cũng sẽ là hơn 20 giây.
- Một nhược điểm khó thấy hơn đó là chuỗi gọi đồng bộ như thế này có thể dẫn đến một mạng lưới phụ thuộc phức tạp và khó kiểm soát. Ví dụ: service ListOrderedProducts gọi User, User lại gọi Q, Q tiếp tục gọi Z và X... và như vậy một request đơn giản từ ListOrderedProducts có thể dẫn đến hàng chục hoặc hàng trăm lời gọi nội bộ phía sau. Và một lần nữa, nếu chỉ một trong các bước đó lỗi, toàn bộ yêu cầu sẽ thất bại.
The easier the investment, the higher the risk
Giao tiếp đồng bộ có những điểm mạnh của riêng nó, nhưng đi kèm là rủi ro vận hành cực kỳ lớn.
Mục đích chính của Microservices luôn luôn khuyên bạn hãy trở thành một Lonely wolf, luôn chơi một mình, không được phụ thuộc vào người khác, tự làm tốt vai trò của chính mình. Còn làm như thế nào á, thế thì phải xem Gia Cát Lượng rùi.
2. Asynchronous Communication (Giao tiếp bất đồng bộ)
Trong mô hình giao tiếp bất đồng bộ, một service có thể gửi yêu cầu đến service khác mà không cần chờ phản hồi ngay lập tức. Thay vì đợi kết quả, service gửi đi tiếp tục xử lý các công việc khác, và phản hồi (nếu có) sẽ được xử lý sau khi nhận được. Ví dụ, Service A gửi một sự kiện hoặc message đến Service B thông qua một cái gọi là Event Bus (Mình sẽ giải thích rõ ở phần dưới), và không cần biết khi nào Service B xử lý xong, đây chính là đặc trưng của Asynchronous Communication.
Chúng ta sẽ có hai cách triển khai. Cách đầu tiên dù là Async nhưng vẫn có nhược điểm giống Sync, nhưng chúng ta vẫn sẽ đi qua 2 cách để hiểu nhé, nhưng trước đó chúng ta cần phải nắm một số thông tin đã.
Event Bus là gì?
Về cơ bản, với giao tiếp bất đồng bộ, ta sẽ thêm một thành phần chung mà mọi service đều có thể kết nối vào. Thành phần này được gọi là một event bus, tạm hiểu là “xe buýt truyền sự kiện”. Mục tiêu của event bus là nhận và phân phối các sự kiện (event) được gửi ra từ các service khác nhau.
Mỗi event giống như một “tờ giấy ghi chú” nói rằng: “Có chuyện gì đó vừa xảy ra” hoặc “cần xử lý việc này”.
Mỗi service kết nối với event bus đều có thể gửi sự kiện ra, hoặc lắng nghe các sự kiện từ service khác.
Cách triển khai với Event Bus đầu tiên
Giả sử Service ListOrderedProducts nhận được yêu cầu: “Hiển thị tất cả sản phẩm mà user #1 đã đặt hàng.”
Bước 1: Gửi Event truy vấn người dùng ListOrderedProducts gửi một event với type là user:query và dữ liệu là userId = 1.
Bước 2: Event Bus chuyển tiếp event đến Service User Event bus chuyển event này đến Service User, nơi chịu trách nhiệm xử lý dữ liệu người dùng.
Bước 3: Service User phản hồi bằng event mới Sau khi xử lý xong, A gửi một event mới chứa thông tin user (ví dụ: Id = 1, name = "Tanjiro").
Bước 4: Event Bus chuyển kết quả về cho Service ListOrderedProducts
Các bước tiếp theo (tương tự)
Service ListOrderedProducts tiếp tục gửi event để lấy đơn hàng từ Service Order, và lấy thông tin sản phẩm từ Service Product.
Cũng ra gì và này nọ đấy, nhưng vẫn chưa tài đâu
Cách này gặp lại những nhược điểm y chang mô hình giao tiếp đồng bộ...
- Các Service vẫn phụ thuộc lẫn nhau: Dù dùng event, nhưng Service ListOrderedProducts vẫn phụ thuộc vào User/Order/Product để xử lý đúng. Nếu User hoặc Order không phản hồi thì ListOrderedProducts cũng “bó tay”.
- Vẫn bị trễ theo service chậm nhất: Tốc độ phản hồi của toàn bộ request vẫn phụ thuộc vào service chậm nhất.
- Mạng lưới phụ thuộc phức tạp: Dễ sinh ra một “mạng nhện” phức tạp giữa các service qua lại bằng event. Khó trace, khó debug.
- Event có thể bị mất: Nếu một event nào đó bị mất hoặc không xử lý kịp, luồng xử lý sẽ bị lỗi hoàn toàn.
Dù vậy service ListOrderedProducts vẫn không cần Database riêng, các service không gọi trực tiếp lẫn nhau và nhiều hệ thống ngoài đời vẫn còn dùng cách này.
Chúng ta sẽ tiếp tục với cách còn lại.
Cách còn lại được anh Stephen gọi nó là: "A Crazy Way of Storing Data"
Ý tưởng của cách này phải ví như mưu lược mà Gia Cát Lượng lấy mũi tên của quân Tào Tháo tại trận Xích Bích vậy. Quá đỉnh cao và điên rồ.
Cách này chúng ta sẽ đạt được pattern "Data per service" và sự độc lập tuyệt đối giữa các service mà chúng ta vẫn theo đuổi.
Ý tưởng:
- Chúng ta sẽ tạo một database riêng cho service ListOrderedProducts để lưu những thông tin cần thiết thực hiện cho nhiệm vụ truy vấn “Hiển thị tất cả sản phẩm mà user đã đặt hàng.”
- Dùng Event bus để đẩy những sự kiện phát ra từ các service để lưu vào database của service ListOrderedProducts.
Ý tưởng chung chung là vậy, chúng ta sẽ để Event bus thu thập các event rồi gửi lại cho service ListOrderedProducts nó lưu, lúc cần thì tự nó lấy ra sử dụng thôi.
Triển khai thử thôi
Hãy xác định rõ chức năng của Service ListOrderedProducts: Với một user ID, service ListOrderedProducts phải trả về tên và ảnh của tất cả sản phẩm người dùng đó đã từng đặt mua.
Giờ chúng ta hãy thiết kế một cấu trúc database để phục vụ mục tiêu này.
Ta sẽ có một bảng users lưu user ID và danh sách ID sản phẩm họ đã mua. Bảng products sẽ lưu ID, tên và ảnh của từng sản phẩm.
Giờ ta chỉ cần tra danh sách ID sản phẩm từ user, rồi lấy thông tin từ bảng sản phẩm là có ngay thông tin người dùng cần.
Tiếp đến là cách lấy dữ liệu và lưu vào database service ListOrderedProducts
Mỗi khi có sản phẩm mới ta tạo một event gửi lên event bus, sau đó để event bus gửi đến cho service ListOrderedProducts:
Tương tự với Service User: khi user đăng ký, nó phát UserCreated chỉ chứa user ID. ListOrderedProducts nhận event và tạo bản ghi user mới, với danh sách sản phẩm rỗng.
Sau đó, khi user mua hàng qua Service Order, nó phát event OrderCreated gồm user ID và product ID. ListOrderedProducts nhận được và cập nhật thêm sản phẩm vào danh sách của user đó.
Bây giờ database của Service ListOrderedProducts đã đầy đủ và hoạt động độc lập. Nó có thể trả lời ngay: "Người dùng X đã đặt gì?" mà không cần gọi ai khác.
Ưu điểm:
- Một service hoàn toàn không phụ thuộc vào các service khác.
- Ngay cả khi các service User, Order, Product gặp sự cố hoặc ngừng hoạt động, Service ListOrderedProducts vẫn hoạt động hoàn toàn bình thường, bởi vì nó độc lập và đã có dữ liệu sẵn.
- Tốc độ phản hồi rất nhanh do Service ListOrderedProducts có sẵn dữ liệu trong database của riêng mình, nên nó có thể truy xuất và phản hồi cực kỳ nhanh chóng mà không cần gọi bất kỳ service nào khác.
Nhược điểm:
Khó hiểu và triển khai vãi cả nồi
Một nhược điểm khác bạn có thể đang thắc mắc đó là sự Trùng lặp dữ liệu (Data duplication) khi mà dữ liệu như thông tin người dùng, sản phẩm hay đơn hàng sẽ được lưu cả ở database gốc của các service Order, User, Product, và một phần trong database của Service ListOrderedProducts.
Nhưng nó không thực sự là nhược điểm khi mà chi phí lưu trữ dự liệu ngày nay với sự phát triển của phần cứng đã khiến cho chi phí lưu trữ không còn là vấn đề. Đây là ví dụ của anh Sờ Ti Phờn:
* Lưu 1GB dữ liệu trên Azure MySQL: khoảng 11.5 cent/tháng
* Trên AWS hay Google Cloud: khoảng 17 cent/tháng
* Vậy dữ liệu một sản phẩm thì chiếm bao nhiêu?
* Tôi lấy một response thật từ API của Amazon cho một sản phẩm (với đầy đủ dữ liệu JSON) – nó chỉ nặng khoảng 1,200 byte.
* Giả sử bạn lưu 100 triệu sản phẩm, chi phí chỉ khoảng 14 đô/tháng.
V. Tổng kết
Trời ơi bài viết này đã thực sự tốn cả 2 tuần của mình chỉ để nghiên cứu và viết lại sao cho dễ hiểu.
Mình xin nhắc lại là bài viết của mình không thể thay thế được những kiến thức khóa học từ anh Sờ Te Phờn, các bạn hãy đăng ký và trải nghiệm thử.
Ở phần sau chúng ta sẽ tiếp đến phần thực hành code để triển khai một mớ lý thuyết chúng ta đã đi từ đầu tới giờ.
Nếu bạn thấy hay thì cho mình xin 1 upvote và để lại comment những chỗ mình đã sai, hoặc muốn thảo luận thêm nhé.
Mình là NekoArcoder, xin hẹn gặp lại các bạn ở bài viết tiếp theo!!!