NoSQL 8 — Chọn store & use case ngân hàng
NoSQL 8 — Chọn store & use case ngân hàng
Bảy bài trước của series đã đi qua tổng quan NoSQL, mô hình document MongoDB, CRUD và index/hiệu năng, rồi Redis căn bản, các pattern và vận hành Redis. Bài cuối này không giới thiệu công nghệ mới. Nó trả lời câu hỏi mà mọi kiến trúc sư dữ liệu ngân hàng phải đối mặt: khi nào dùng cái gì, và làm sao để nhiều store sống chung mà không loạn?
Thông điệp trung tâm là polyglot persistence — dùng nhiều loại kho lưu trữ khác nhau trong cùng một hệ thống, mỗi loại giải một bài toán mà nó giỏi nhất. Ngược lại với nó là ảo tưởng "một cỡ cho tất cả" (one size fits all): ép mọi thứ vào một RDBMS duy nhất, hoặc tệ hơn, nghe theo hype mà nhét mọi thứ vào MongoDB. Cả hai cực đoan đều trả giá.
Lưu ý về sandbox: SQL sandbox của Knowledge Base chạy trên PostgreSQL (chỉ đọc). Trong bài này, các block SQL đánh dấu "▶ Chạy được" minh hoạ đúng vai trò của RDBMS làm nguồn sự thật (source of truth) cho dữ liệu quan hệ. Các đoạn giả lệnh MongoDB/Redis/ClickHouse chỉ minh hoạ cú pháp, không chạy trên sandbox này.
Vì sao không "một cỡ cho tất cả"
Mỗi loại store tối ưu cho một mô hình truy cập khác nhau, và tối ưu chỗ này đồng nghĩa hy sinh chỗ khác:
- RDBMS tối ưu cho giao dịch ACID và truy vấn quan hệ phức tạp qua nhiều bảng. Đổi lại, schema cứng và mở rộng ngang khó.
- Document store (MongoDB) tối ưu cho đọc/ghi một thực thể lồng nhau với schema linh hoạt. Đổi lại, không có giao dịch phân tán mạnh như RDBMS truyền thống, JOIN yếu, dễ trùng lặp dữ liệu.
- Key-value in-memory (Redis) tối ưu cho truy cập theo khoá cực nhanh, độ trễ dưới mili-giây. Đổi lại, dữ liệu nằm trong RAM (đắt, giới hạn), không phải nơi giữ sự thật.
- Columnar OLAP (ClickHouse) tối ưu cho quét và tổng hợp hàng tỷ dòng. Đổi lại, cập nhật/xoá điểm rất kém, không hợp giao dịch.
- Search engine (Elasticsearch) tối ưu cho tìm kiếm toàn văn và truy vấn theo nhiều mặt (facet). Đổi lại, không phải cơ sở dữ liệu giao dịch, dữ liệu là bản sao đánh chỉ mục.
Không store nào giỏi mọi thứ. Chọn sai store không chỉ chậm — nó có thể khiến bạn mất dữ liệu tiền hoặc vi phạm tính nhất quán, điều không thể chấp nhận trong ngân hàng.
Bảng QUYẾT ĐỊNH chọn store
Đây là bảng tra cứu nhanh khi thiết kế một thành phần mới. Cột "Không dùng cho" quan trọng ngang cột "Dùng cho".
| Store | Mô hình | Dùng cho (use case) | KHÔNG dùng cho |
|---|---|---|---|
| RDBMS / PostgreSQL | Quan hệ, ACID | Sổ cái, tài khoản, giao dịch tiền lõi; số dư; báo cáo tuân thủ; mọi dữ liệu cần toàn vẹn tham chiếu và JOIN phức tạp | Cache nóng; tìm kiếm toàn văn; analytic quét tỷ dòng; blob nội dung lớn |
| MongoDB (document) | Document JSON lồng nhau, schema linh hoạt | Hồ sơ khách hàng 360 (gộp nhiều nguồn); catalog sản phẩm với thuộc tính không đồng nhất; nội dung/CMS; event store (append sự kiện); form/khảo sát cấu trúc thay đổi | Sổ cái tiền; báo cáo cần JOIN nhiều bảng; đối soát cần giao dịch mạnh |
| Redis (in-memory KV) | Key-value, cấu trúc dữ liệu | Cache trước DB; session; rate limiting; counter; leaderboard/xếp hạng real-time; distributed lock; hàng đợi nhẹ | Nguồn sự thật cho dữ liệu tiền; lưu trữ lâu dài; truy vấn phức tạp không theo khoá |
| ClickHouse (columnar OLAP) | Cột, nén cao | Phân tích khối lượng lớn: log giao dịch, hành vi khách, tổng hợp theo thời gian, dashboard analytic — xem ClickHouse tổng quan | Giao dịch; cập nhật/xoá điểm thường xuyên; nguồn sự thật |
| Elasticsearch | Chỉ mục đảo (inverted index) | Tìm kiếm toàn văn (khách, giao dịch, tài liệu); log/observability; truy vấn theo facet — xem Elasticsearch tổng quan | Nguồn sự thật; giao dịch tiền; dữ liệu cần đọc-ngay-sau-ghi tuyệt đối |
Quy tắc ngón tay cái: tiền và sự thật ở RDBMS; mọi thứ khác là dẫn xuất (derived) từ đó. Redis, Elasticsearch, ClickHouse và thường cả nhiều collection MongoDB đều là bản sao được tối ưu cho một kiểu đọc — chúng có thể rebuild lại từ nguồn sự thật khi cần.
Kiến trúc tham chiếu ngân hàng nhiều store
Một hệ thống ngân hàng số thực tế hiếm khi dùng một store. Sơ đồ dưới mô tả cách phối hợp: RDBMS là lõi giao dịch, Redis đứng trước làm lớp tốc độ, MongoDB giữ dữ liệu linh hoạt, còn ClickHouse/Elasticsearch nhận dữ liệu dẫn xuất cho analytic và tìm kiếm. Luồng đồng bộ đi qua Change Data Capture (CDC) và message bus.
Điểm mấu chốt của kiến trúc: mũi tên đồng bộ đi một chiều, từ nguồn sự thật ra các store dẫn xuất, qua message bus như Kafka. Không có store dẫn xuất nào được phép ghi ngược tuỳ tiện vào lõi.
Vai trò từng lớp
- PostgreSQL / Core — nơi duy nhất dữ liệu tiền được ghi. Mọi thay đổi số dư, chuyển khoản, hạch toán đều là giao dịch ACID ở đây. Xem kiến trúc PostgreSQL để hiểu WAL và MVCC bảo đảm điều đó.
- Redis — đứng trước lõi. Cache số dư/thông tin ít đổi, giữ session, đếm rate limit. Khi lõi thay đổi, Redis bị invalidate (xoá key) chứ không tự sinh dữ liệu mới.
- MongoDB — giữ những thứ RDBMS mô hình hoá vụng: hồ sơ khách 360 gộp từ nhiều hệ thống, catalog sản phẩm với thuộc tính không đồng nhất, event store append-only.
- ClickHouse / Elasticsearch — nhận bản sao đã tối ưu để phục vụ dashboard analytic và tìm kiếm, giảm tải cho lõi.
RDBMS làm nguồn sự thật: minh hoạ
Để thấy vì sao dữ liệu quan hệ nên ở RDBMS, hãy nhìn một truy vấn tổng hợp qua nhiều bảng — điều mà document store làm rất chật vật. Câu dưới lấy tổng số dư và tổng giá trị giao dịch của từng khách hàng theo thành phố, ghép qua các khoá quan hệ. Đây chính là loại "báo cáo có JOIN" thuộc về RDBMS.
-- ▶ Chạy được
SELECT
c.city,
c.full_name,
a.currency,
COUNT(DISTINCT a.id) AS so_tai_khoan,
SUM(a.balance) AS tong_so_du,
COUNT(t.id) AS so_giao_dich,
COALESCE(SUM(t.amount), 0) AS tong_gia_tri_gd
FROM customers c
JOIN accounts a ON a.customer_id = c.id
LEFT JOIN transactions t ON t.account_id = a.id
GROUP BY c.city, c.full_name, a.currency
ORDER BY tong_so_du DESC;
Câu này chạm 3 bảng (customers, accounts, transactions) qua đúng khoá join (accounts.customer_id → customers.id, transactions.account_id → accounts.id). Trong MongoDB, để có cùng kết quả bạn hoặc phải nhồi (embed) giao dịch vào document khách — dẫn tới document phình vô hạn — hoặc dùng $lookup (JOIN mô phỏng) vốn chậm và giới hạn. RDBMS sinh ra để làm việc này.
Câu thứ hai minh hoạ vai trò báo cáo tuân thủ: liệt kê các giao dịch lớn kèm tên khách và thành phố, ghép qua đúng chuỗi khoá transactions → accounts → customers. Đây là kiểu truy vấn kiểm toán mà chỉ RDBMS đáp ứng gọn gàng — nhắc lại: bảng transactions/accounts không chứa tên khách, muốn có tên bắt buộc phải JOIN sang customers.
-- ▶ Chạy được
SELECT
c.full_name AS khach_hang,
c.city,
a.account_no,
a.currency,
t.kind,
t.amount,
t.created_at
FROM transactions t
JOIN accounts a ON a.id = t.account_id
JOIN customers c ON c.id = a.customer_id
WHERE t.amount >= 100000000
ORDER BY t.amount DESC;
Kết quả cho thấy: một truy vấn duy nhất gom dữ liệu qua ba bảng theo quan hệ khoá, ra báo cáo kiểm toán giao dịch lớn. Không store NoSQL nào làm việc này tự nhiên bằng RDBMS — đó là lý do dữ liệu quan hệ lõi phải ở lại đây.
Nhất quán giữa các store
Khi một sự thật tồn tại nhiều bản (số dư ở PostgreSQL, bản cache ở Redis, bản đánh chỉ mục ở Elasticsearch), câu hỏi sống còn là: làm sao chúng khớp nhau? Có ba cách tiếp cận, và một trong số đó là cái bẫy.
1. CDC / event-driven (khuyến nghị)
Ứng dụng chỉ ghi vào một nguồn sự thật (PostgreSQL). Một tiến trình Change Data Capture đọc WAL/binlog của DB, phát sự kiện lên message bus, và các store khác subscribe để cập nhật bản sao của mình. Ưu điểm: ứng dụng không cần biết có bao nhiêu store hạ nguồn; thêm store mới chỉ là thêm consumer; thứ tự sự kiện được bảo toàn theo log.
App --ghi--> PostgreSQL --WAL--> CDC --event--> Kafka --> [Redis invalidate | ES index | ClickHouse insert]
Đây là mô hình chuẩn cho hệ ngân hàng nhiều store, và là lý do Kafka thường nằm ở trung tâm.
2. Cache invalidation
Với Redis đứng trước DB, chiến lược an toàn là cache-aside + invalidate: đọc thì thử cache trước, miss thì đọc DB rồi điền cache; ghi thì cập nhật DB rồi xoá key cache (chứ không cập nhật cache). Lần đọc sau sẽ điền lại giá trị mới từ DB. Đặt TTL hợp lý để cache tự hết hạn kể cả khi lỡ invalidate. Chi tiết pattern ở Redis patterns.
3. Dual-write (NGUY HIỂM — tránh)
Cám dỗ lớn nhất: cho ứng dụng ghi thẳng vào hai store cùng lúc — ví dụ ghi PostgreSQL rồi ghi luôn Elasticsearch trong cùng hàm. Vấn đề: hai lệnh ghi không nằm trong một giao dịch. Nếu ghi PostgreSQL thành công nhưng ghi Elasticsearch fail (network, timeout, crash giữa chừng), hai store lệch nhau vĩnh viễn mà không ai biết. Trong ngân hàng, dual-write giữa hệ tiền và hệ khác là con đường ngắn nhất tới lệch sổ. Luôn thay dual-write bằng CDC/event: ghi một nơi, lan toả qua log.
| Cách | Nhất quán | Rủi ro | Khi dùng |
|---|---|---|---|
| CDC / event | Eventual, đáng tin | Độ trễ nhỏ giữa các store | Mặc định cho hệ nhiều store |
| Cache invalidation | Eventual, có TTL bảo hiểm | Cửa sổ stale ngắn | Redis trước DB |
| Dual-write | Dễ lệch vĩnh viễn | Cao — không atomic | Tránh, nhất là với dữ liệu tiền |
Use case thực tế
Bối cảnh. NCB triển khai màn hình "Chi tiết khách hàng 360" trên app cán bộ quan hệ khách hàng (RM). Mỗi lần mở, màn hình cần: (a) danh sách tài khoản và số dư hiện tại, (b) 20 giao dịch gần nhất, (c) hồ sơ 360 gộp từ CRM + core + kênh số, (d) ô tìm kiếm giao dịch theo mô tả. Ban đầu team nhét tất cả vào PostgreSQL; đỉnh giờ giao dịch màn hình tải 2.8 giây, DB core bị nghẽn vì truy vấn tìm kiếm toàn văn.
Thiết kế lại theo polyglot persistence:
- Số dư & tài khoản (nguồn sự thật) — vẫn ở PostgreSQL. Nhưng đặt Redis cache-aside phía trước với TTL 30 giây cho số dư đọc-nóng. Khi có giao dịch, CDC phát sự kiện invalidate key số dư của tài khoản đó. Kết quả: p95 đọc số dư từ ~180 ms xuống ~4 ms.
- Hồ sơ 360 — chuyển sang MongoDB, mỗi khách một document gộp sẵn dữ liệu từ CRM/core/kênh số, cập nhật qua change stream. Màn hình đọc một document thay vì JOIN 6 bảng.
- Tìm kiếm giao dịch theo mô tả — đẩy sang Elasticsearch, đánh chỉ mục từ CDC của bảng
transactions. Ô tìm kiếm không còn chạm DB core. - Báo cáo/kiểm toán và đối soát cuối ngày — giữ nguyên trên PostgreSQL bằng các truy vấn JOIN như minh hoạ ở trên, vì đây là nơi cần sự thật và toàn vẹn.
Kết quả (minh hoạ theo mô hình): thời gian tải màn hình từ 2.8 s xuống ~0.4 s; tải đọc trên DB core giảm khoảng 70% nhờ Redis chặn đọc nóng và Elasticsearch gánh tìm kiếm; DB core rảnh tay hơn cho giao dịch ghi. Quan trọng: không một xu nào rời khỏi PostgreSQL — mọi số dư và giao dịch vẫn được ghi và đối soát trên hệ ACID, các store khác chỉ là bản sao dẫn xuất có thể rebuild.
Lưu ý riêng cho ngân hàng
- Tiền và giao dịch bắt buộc ở hệ ACID. NoSQL chỉ cho vai trò phụ trợ (cache, hồ sơ, tìm kiếm, analytic). Đừng bao giờ để số dư "sống" ở Redis hay MongoDB làm nguồn sự thật.
- Tuân thủ & bảo mật khi dùng NoSQL. Nhiều triển khai NoSQL mặc định không bật xác thực/mã hoá. Với dữ liệu khách hàng: bật mã hoá khi truyền (TLS) và khi lưu (at-rest), bật xác thực, phân quyền tối thiểu (least privilege) theo role. Tham chiếu kiểm soát truy cập.
- Đừng quên chất lượng dữ liệu ở store dẫn xuất. Bản sao ở ES/ClickHouse có thể lệch nếu CDC gãy; cần giám sát độ trễ replication và có cách rebuild — liên quan chất lượng dữ liệu.
Cạm bẫy thường gặp
- Chọn NoSQL vì hype. "Ai cũng dùng Mongo" không phải lý do kỹ thuật. Nếu dữ liệu có quan hệ mạnh và cần giao dịch, RDBMS vẫn đúng. Bắt đầu từ mô hình truy cập, không từ tên công nghệ.
- Mất giao dịch. Đặt dữ liệu tiền vào store không ACID rồi giả định nó an toàn. Kết quả: race condition, ghi mất, số dư âm không giải thích được.
- Mô hình sai store. Nhồi quan hệ nhiều-nhiều vào document (document phình, cập nhật lan man), hoặc ép analytic quét tỷ dòng vào RDBMS OLTP (khoá bảng, chậm). Mỗi store có hình dạng dữ liệu tự nhiên của nó.
- Dual-write. Ghi thẳng nhiều store trong một hàm — nguồn gốc lệch dữ liệu vĩnh viễn. Luôn thay bằng CDC/event.
- Quá nhiều store. Polyglot không có nghĩa là càng nhiều càng tốt. Mỗi store thêm vào là thêm vận hành, backup, giám sát, người biết. Chỉ thêm khi có nhu cầu thực sự mà store hiện có không đáp ứng.
Ghi nhớ
- Polyglot persistence: chọn đúng store cho từng nhu cầu; không "một cỡ cho tất cả". Bắt đầu từ mô hình truy cập, không từ tên công nghệ.
- Bảng quyết định: RDBMS cho giao dịch/quan hệ/báo cáo ACID; MongoDB cho document linh hoạt (hồ sơ 360, catalog, nội dung, event store); Redis cho cache/session/rate-limit/counter/leaderboard; ClickHouse cho analytic khối lượng lớn; Elasticsearch cho tìm kiếm/log.
- Nguồn sự thật duy nhất: tiền và dữ liệu quan hệ lõi ở RDBMS; mọi store khác là bản sao dẫn xuất, rebuild được.
- Nhất quán: ưu tiên CDC/event qua message bus; dùng cache invalidation cho Redis; tránh dual-write — nó gây lệch vĩnh viễn.
- Ngân hàng: dữ liệu tiền/giao dịch bắt buộc ở hệ ACID; NoSQL chỉ phụ trợ; bật mã hoá/xác thực/phân quyền tối thiểu khi dùng NoSQL.
- Cạm bẫy: chọn theo hype, mất giao dịch, mô hình sai store, dual-write, và ôm quá nhiều store làm nặng vận hành.
Bài viết liên quan
Cách index hoạt động (B-Tree), đọc EXPLAIN, seq scan vs index scan và mẫu tối ưu truy vấn.
Khoá, ràng buộc, quan hệ và chuẩn hoá 1NF/2NF/3NF — thiết kế lược đồ đúng từ đầu.
Kết nhiều bảng đúng cách: các loại JOIN, bẫy thường gặp và phép hợp tập.
Truy vấn lồng, CTE (WITH), CTE đệ quy và hàm cửa sổ — vũ khí cho phân tích nâng cao.