Mọi người nhớ ủng hộ bài viết gốc ở blog https://duthaho.substack.com/p/toi-i-phong-van-system-design-ve của mình nhé
Bắt đầu buổi phỏng vấn
(Anh Minh): Chào duthaho, tôi là Minh, Kỹ sư Phần mềm Cấp cao tại công ty Z. Cảm ơn bạn đã tham gia buổi phỏng vấn hôm nay. Trong khoảng 45 phút tới, chúng ta sẽ cùng nhau thảo luận về một bài toán thiết kế hệ thống. Mục tiêu chính là để tôi hiểu hơn về tư duy giải quyết vấn đề, cách bạn phân tích các ưu nhược điểm (trade-offs) và đưa ra quyết định về mặt kiến trúc.
Bạn cứ thoải mái trình bày suy nghĩ, vẽ sơ đồ nếu cần, và đặt câu hỏi nhé. Đây là một buổi thảo luận mở. Bạn có câu hỏi gì trước khi chúng ta bắt đầu không ạ?
(duthaho): Dạ chào anh Minh, em đã sẵn sàng ạ. Em không có câu hỏi gì thêm.
(Anh Minh): Được rồi, chúng ta bắt đầu nhé.
Trình bày vấn đề
(Anh Minh): Giả sử công ty chúng ta có một hệ thống thương mại điện tử. Thành phần cốt lõi là một bảng database tên là orders, chứa dữ liệu về các đơn hàng. Bảng này hiện tại rất lớn, khoảng 500 triệu bản ghi và tốc độ tăng trưởng rất nhanh, khoảng 1 triệu bản ghi mới mỗi ngày.
Yêu cầu đặt ra là: đội ngũ sản phẩm muốn có chức năng "xóa mềm" (soft delete) cho các đơn hàng.
Bạn hãy thiết kế một giải pháp cho yêu cầu này.
(duthaho): Dạ vâng. Đây là một bài toán rất thú vị. Trước khi đi vào giải pháp, em xin phép được đặt một vài câu hỏi để làm rõ yêu cầu ạ.
(Anh Minh): Rất tốt, mời bạn.
(duthaho):
-
Về chức năng: Mục đích chính của việc xóa mềm này là gì ạ? Có phải để cho người dùng cuối tự khôi phục, hay chỉ cho admin sử dụng để khôi phục khi có sự cố, hay là để phục vụ mục đích audit trail?
-
Dữ liệu sau khi xóa mềm có cần được truy vấn thường xuyên không? Và ai là người có quyền truy vấn?
-
Về phi chức năng: Chính sách lưu trữ (retention policy) cho dữ liệu đã xóa mềm là bao lâu? Ví dụ 30 ngày, 90 ngày, hay vĩnh viễn? Sau thời gian đó có cần xóa vĩnh viễn không?
-
Và quan trọng nhất, yêu cầu về hiệu năng đối với các truy vấn trên dữ liệu "còn sống" (active data) là như thế nào? Thao tác xóa mềm có được phép làm ảnh hưởng đến hiệu năng của các giao dịch đang diễn ra không ạ?
(Anh Minh): Những câu hỏi rất xác đáng, duthaho. Chúng ta hãy làm việc với giả định sau:
-
Mục đích: Chủ yếu để admin có thể khôi phục dữ liệu trong vòng 30 ngày. Dữ liệu đã xóa không cần truy vấn thường xuyên.
-
Xóa vĩnh viễn: Sau 30 ngày, dữ liệu cần được xóa vĩnh viễn (purge).
-
Hiệu năng: Yêu cầu quan trọng nhất là hiệu năng của các truy vấn trên dữ liệu active (ví dụ: xem danh sách đơn hàng, cập nhật trạng thái đơn hàng) gần như không được bị ảnh hưởng.
Với những thông tin này, bạn sẽ bắt đầu như thế nào?
Thảo luận giải pháp
(duthaho): Dạ, với yêu cầu đó, em thấy có một vài hướng tiếp cận với các trade-off khác nhau.
-
Hướng 1: Dùng cột cờ (Flag-based). Đây là cách đơn giản nhất, ta thêm một cột deleted_at vào bảng orders. Khi xóa thì UPDATE cột này, và mọi câu SELECT sẽ phải thêm điều kiện WHERE deleted_at IS NULL.
-
Hướng 2: Di chuyển sang bảng lưu trữ (Archive Table). Ta tạo một bảng orders_archive có cấu trúc tương tự. Khi xóa, ta INSERT bản ghi vào bảng archive rồi DELETE khỏi bảng chính.
(Anh Minh): OK. Bạn có thể phân tích sâu hơn về ưu và nhược điểm của hai cách này, đặc biệt là với bảng orders 500 triệu bản ghi không?
(duthaho): Dạ được ạ.
-
Với Hướng 1, ưu điểm là đơn giản và dễ khôi phục. Nhưng nhược điểm với bảng lớn là rất nghiêm trọng. Index sẽ bị phình to (bloating) vì phải chứa cả dữ liệu đã xóa, khiến các thao tác đọc và ghi trên bảng chính ngày càng chậm. Việc xóa vĩnh viễn bằng DELETE ... WHERE deleted_at < ... sẽ là một cơn ác mộng, có thể gây lock bảng và ảnh hưởng toàn hệ thống.
-
Với Hướng 2, ưu điểm là nó giải quyết được vấn đề cốt lõi: giữ cho bảng orders chính luôn gọn nhẹ và hiệu năng cao. Tuy nhiên, nhược điểm là thao tác xóa trở nên phức tạp hơn, cần xử lý trong transaction để đảm bảo toàn vẹn.
Vì yêu cầu đặt nặng về hiệu năng cho bảng chính, em sẽ nghiêng về Hướng 2. Tuy nhiên, em muốn đề xuất một kiến trúc cải tiến hơn một chút để nó linh hoạt và đáng tin cậy hơn.
(Anh Minh): Rất hay. Tôi đang chờ nghe đây.
(duthaho): Em đề xuất một kiến trúc hướng sự kiện (Event-Driven).
-
Khi API nhận yêu cầu xóa, nó sẽ không xóa trực tiếp mà chỉ gửi một message chứa order_id cần xóa vào một Message Queue (ví dụ: RabbitMQ hoặc Kafka). Sau đó trả về 202 Accepted ngay cho người dùng.
-
Sẽ có một nhóm các service worker riêng biệt lắng nghe từ queue này.
-
Khi nhận được message, worker sẽ thực hiện công việc di chuyển dữ liệu từ orders sang orders_archive trong một transaction.Kiến trúc này giúp tách rời tác vụ xóa khỏi luồng request chính, đảm bảo API phản hồi nhanh và hệ thống có khả năng mở rộng tốt bằng cách tăng số lượng worker.
(Anh Minh): Một giải pháp rất tốt, nó giải quyết được vấn đề về hiệu năng và khả năng mở rộng. Giờ hãy đi sâu hơn vào một vài tình huống thực tế nhé.
Giả sử có yêu cầu xóa toàn bộ đơn hàng của một khách hàng lớn, có thể lên tới hàng chục nghìn đơn. Việc gửi hàng chục nghìn message vào queue có vẻ không tối ưu. Bạn sẽ xử lý yêu cầu xóa theo lô (batch delete) này như thế nào?
(duthaho): Dạ, đó là một vấn đề rất thực tế. Thay vì gửi N message, em sẽ thiết kế một hệ thống xử lý job bất đồng bộ.
-
Tạo một bảng mới là batch_jobs để lưu thông tin về các job (loại job, điều kiện, trạng thái, tiến trình).
-
API sẽ tạo một bản ghi trong bảng này, rồi gửi một message duy nhất chứa job_id vào một queue dành riêng cho các job nặng.
-
Worker chuyên xử lý batch sẽ nhận job, đọc điều kiện và xử lý xóa theo từng chunk nhỏ (ví dụ 10,000 bản ghi/lần) để tránh các transaction quá lớn gây lock database.
(Anh Minh): Rất chi tiết. Vậy với hệ thống batch job đó, nếu một job lớn đang chạy và có một job nhỏ nhưng khẩn cấp (ví dụ: yêu cầu xóa dữ liệu theo GDPR) cần được thực thi ngay, bạn làm thế nào để ưu tiên job khẩn cấp?
(duthaho): Để xử lý ưu tiên, em sẽ không dùng một queue chung nữa mà chia thành nhiều queue, ví dụ: high_priority_queue và normal_priority_queue. Sẽ có các nhóm worker riêng biệt cho từng queue, đảm bảo các job quan trọng không bao giờ bị chặn bởi các job thông thường.
(Anh Minh): Tuyệt vời. Câu hỏi cuối cùng về mặt kỹ thuật: Job xóa có thể chạy trong hàng giờ. Làm sao để người dùng (admin) có thể hủy (cancel) một job đang chạy nếu họ phát hiện ra sai sót?
(duthaho): Để hủy job, em sẽ dùng một cơ chế "hợp tác".
-
API hủy sẽ cập nhật trạng thái của job trong bảng batch_jobs thành CANCELING.
-
Worker ở đầu mỗi vòng lặp xử lý chunk mới sẽ kiểm tra trạng thái của job trong DB.
-
Nếu thấy trạng thái là CANCELING, nó sẽ dừng lại, cập nhật trạng thái cuối cùng là CANCELED và thoát.Để tránh query DB liên tục, có thể dùng cơ chế Redis Pub/Sub. Worker sẽ SUBSCRIBE vào một kênh của Redis, và API PUBLISH một tín hiệu hủy vào kênh đó, giúp worker nhận được thông báo gần như tức thời.
Tổng kết
(Anh Minh): Cảm ơn duthaho. Phần trình bày của bạn rất ấn tượng. Bạn đã đi từ một yêu cầu khá mơ hồ, chủ động đặt câu hỏi để làm rõ, phân tích các hướng tiếp cận khác nhau, đề xuất một kiến trúc vững chắc và xử lý được các trường hợp phức tạp như batch job, ưu tiên và hủy tác vụ.
Tôi đã có đủ thông tin mình cần rồi. Bây giờ, bạn có câu hỏi nào muốn hỏi tôi về công ty, đội ngũ hay dự án không?
(duthaho): Dạ em cảm ơn anh Minh. Em muốn hỏi thêm về...