Read path và write path: Tối ưu đọc, ghi trong cơ sở dữ liệu
Trả lời nhanh: read path và write path là chuỗi bước cơ sở dữ liệu thực hiện để đọc hoặc ghi dữ liệu. Index, cache và bản sao thường rút ngắn đường đọc nhưng làm tăng công việc khi ghi. WAL và LSM-tree giúp nhận ghi nhanh hơn nhưng có thể chuyển chi phí sang compaction và truy vấn. Cách tối ưu đúng phải dựa trên workload đo được.
Cập nhật lần cuối: 20/08/2026
Một câu lệnh SELECT và một câu lệnh INSERT có vẻ chỉ khác nhau ở động từ. Ở bên trong hệ thống lưu trữ, chúng đi qua hai hành trình rất khác nhau.
Khi đọc, hệ thống phải tìm đúng phiên bản dữ liệu, kiểm tra index, cache, file trên đĩa và đôi khi hợp nhất nhiều nguồn. Khi ghi, hệ thống phải bảo vệ tính bền vững, cập nhật cấu trúc liên quan, xử lý cạnh tranh và chuẩn bị cho những lần đọc sau.
Vấn đề cốt lõi là: một tối ưu giúp đọc nhanh thường khiến ghi nặng hơn, còn một thiết kế hấp thụ ghi nhanh thường bắt quá trình đọc hoặc bảo trì nền làm thêm việc.
Hiểu read path và write path giúp ta tránh kiểu tối ưu theo triệu chứng: thấy truy vấn chậm thì thêm index, thấy database nóng thì thêm replica, thấy dashboard chậm thì thêm cache. Mỗi lựa chọn đều có hóa đơn đi kèm.
Read path và write path là gì?
Read path là toàn bộ chuỗi xử lý từ lúc ứng dụng yêu cầu dữ liệu đến khi nhận kết quả. Write path là chuỗi xử lý từ lúc ứng dụng gửi thay đổi đến khi hệ thống xác nhận thay đổi đó theo mức độ bền vững và nhất quán đã cam kết.
Một read path điển hình có thể gồm:
- nhận và phân tích câu truy vấn;
- chọn kế hoạch thực thi;
- kiểm tra cache hoặc buffer pool;
- tìm trong index;
- đọc page, memtable hoặc SSTable;
- kiểm tra phiên bản bản ghi nhìn thấy được;
- lọc, join, sắp xếp và trả kết quả.
Một write path điển hình có thể gồm:
Đường đi thực tế phụ thuộc loại database. PostgreSQL dùng heap, B-tree, MVCC và Write-Ahead Logging. Cassandra và RocksDB dựa trên họ cấu trúc Log-Structured Merge Tree, thường gọi là LSM-tree. Hai hướng thiết kế phân bổ chi phí đọc ghi theo cách khác nhau.
Vì sao tối ưu đọc và tối ưu ghi thường xung đột?
Muốn đọc nhanh, ta thường chuẩn bị sẵn câu trả lời hoặc rút ngắn không gian tìm kiếm. Điều đó tạo ra dữ liệu phụ phải được cập nhật mỗi khi nguồn thay đổi.
Ví dụ, một bảng đơn hàng có thể có:
- index theo
customer_idđể tìm đơn của khách; - index theo
statusđể vận hành kho; - materialized view theo ngày cho dashboard;
- cache tổng doanh thu;
- search index để nhân viên tìm kiếm;
- read replica để chia tải truy vấn.
Một lần tạo đơn không còn chỉ ghi một hàng. Nó có thể cập nhật nhiều index, phát sự kiện, làm mất hiệu lực cache và chờ replica nhận dữ liệu. Đây là write amplification, tức một thay đổi logic tạo ra nhiều thao tác vật lý hoặc dữ liệu dẫn xuất.
Ở chiều ngược lại, muốn ghi nhanh, hệ thống có thể ghi tuần tự vào log và bộ nhớ, trì hoãn sắp xếp hoặc hợp nhất. Lúc đọc, nó phải kiểm tra nhiều cấu trúc và nhiều file hơn. Đây là read amplification.
| Lựa chọn | Lợi cho | Chi phí chuyển sang |
|---|---|---|
| Thêm index | Truy vấn lọc, join, sắp xếp | Ghi, lưu trữ, vacuum và cache |
| Thêm cache | Độ trễ đọc và tải database | Invalidation, dữ liệu cũ, vận hành |
| Thêm read replica | Throughput đọc | Replication lag, consistency, chi phí |
| Materialized view | Báo cáo và truy vấn tổng hợp | Refresh, ghi dẫn xuất, độ trễ cập nhật |
| LSM-tree | Ghi tuần tự, throughput ghi | Compaction và nhiều lớp khi đọc |
| Denormalization | Ít join khi đọc | Đồng bộ bản sao dữ liệu khi ghi |
Không có lựa chọn miễn phí. Câu hỏi đúng không phải “cách nào nhanh nhất?”, mà là “chúng ta chấp nhận trả chi phí ở đâu?”.
Write path của B-tree hoạt động thế nào?
B-tree duy trì dữ liệu index trong cấu trúc đã sắp xếp. Khi thêm hoặc sửa một khóa, database tìm page phù hợp, cập nhật nó và đôi khi tách page. Với database quan hệ, một thay đổi ở bảng còn có thể kéo theo nhiều index phụ.
PostgreSQL dùng Write-Ahead Logging. Theo tài liệu chính thức, thay đổi của data file chỉ được ghi sau khi bản ghi WAL mô tả thay đổi đã được flush vào vùng lưu trữ bền vững. Nhờ đó, sau sự cố hệ thống có thể replay log để khôi phục trạng thái hợp lệ.
Một cách nhìn đơn giản:
INSERT/UPDATE
-> kiểm tra constraint
-> ghi WAL
-> sửa page trong buffer
-> cập nhật các index
-> commit
-> checkpoint ghi page bẩn xuống đĩa
WAL biến nhiều thao tác ghi ngẫu nhiên thành dòng log tuần tự thuận lợi hơn cho thiết bị lưu trữ. Tuy nhiên, dữ liệu cuối cùng vẫn phải đi vào table và index. Nếu một bảng có quá nhiều index, write path sẽ chịu thêm CPU, I/O và contention.
MVCC tác động gì đến đọc và ghi?
PostgreSQL sử dụng Multiversion Concurrency Control. Tài liệu PostgreSQL giải thích rằng đọc không xung đột với ghi theo mô hình MVCC. Thay vì sửa đè một phiên bản mà mọi transaction đang nhìn, hệ thống giữ nhiều phiên bản và chọn phiên bản phù hợp với snapshot.
Lợi ích là mức độ đồng thời cao. Chi phí là:
Đây là ví dụ rõ ràng về chi phí bị dời sang công việc nền. Nếu chỉ đo latency của câu lệnh ghi mà bỏ qua vacuum, bloat và checkpoint, ta chưa đo đầy đủ write path.
Write path của LSM-tree hoạt động thế nào?
LSM-tree ưu tiên hấp thụ ghi bằng cách ghi tuần tự vào log và thêm dữ liệu vào cấu trúc trong bộ nhớ. Khi memtable đạt ngưỡng, hệ thống flush nó thành Sorted String Table, thường gọi là SSTable. Các file được hợp nhất trong quá trình compaction.
PUT
-> commit log hoặc WAL
-> memtable
-> xác nhận
-> flush thành SSTable
-> compaction theo nền
Tài liệu Apache Cassandra mô tả storage engine của Cassandra được tối ưu cho workload thiên về ghi, dùng LSM-tree và cách tiếp cận append-only. Đổi lại, read path và write amplification do compaction trở thành phần quan trọng của bài toán.
LSM-tree không loại bỏ chi phí. Nó trì hoãn và gom chi phí:
- dữ liệu có thể xuất hiện ở memtable và nhiều SSTable;
- cùng một key có thể có nhiều phiên bản;
- xóa thường tạo tombstone thay vì loại bỏ ngay;
- compaction đọc và ghi lại lượng dữ liệu lớn;
- khi compaction chậm, số file tăng và read path xấu đi.
Vì vậy, throughput ghi cao ở thời điểm request chưa đồng nghĩa hệ thống khỏe. Cần theo dõi compaction backlog, disk bandwidth, số SSTable và tail latency của đọc.
Read path của B-tree diễn ra ra sao?
Với truy vấn có index phù hợp, database đi từ root qua các nhánh của B-tree để tìm leaf page, sau đó truy cập row hoặc page dữ liệu. Nếu page đã nằm trong buffer pool, không cần đọc đĩa. Nếu query cần nhiều hàng, planner có thể chọn sequential scan vì đọc tuần tự cả bảng rẻ hơn hàng nghìn lần truy cập ngẫu nhiên.
Read path không chỉ là “có index hay không”. Nó còn phụ thuộc:
- độ chọn lọc của điều kiện;
- thống kê có phản ánh dữ liệu hiện tại không;
- page có trong cache không;
- query có trả nhiều cột không;
- index có bao phủ truy vấn không;
- join order và cardinality estimate;
- số phiên bản bản ghi phải kiểm tra;
- contention trên CPU, bộ nhớ và storage.
Một index giúp query A nhanh có thể không giúp query B, nhưng mọi lần ghi vẫn phải duy trì index đó. Vì thế, index không được dùng là khoản thuế vĩnh viễn lên write path.
Read path của LSM-tree diễn ra ra sao?
Khi tìm một key trong LSM-tree, hệ thống thường kiểm tra mutable memtable, immutable memtable và các tầng SSTable. Bloom filter giúp loại nhanh những file chắc chắn không chứa key. Block cache giữ các block hay dùng trong RAM.
Tài liệu RocksDB về MultiGet mô tả read path bắt đầu từ memtable, sau đó đến immutable memtable rồi tra cứu SST file theo từng level. Ở level 0, nhiều file có thể chồng lấn nên hệ thống có thể phải kiểm tra nhiều file theo thứ tự mới nhất trước.
Read path trở nên đắt khi:
- có quá nhiều SSTable;
- cache hit rate thấp;
- Bloom filter không được cấu hình phù hợp;
- tombstone dày;
- compaction chưa theo kịp;
- range scan phải merge nhiều iterator;
- giá trị lớn khiến nhiều block phải đọc.
Do đó, một database “write optimized” không mặc định cho mọi workload nhiều ghi. Nếu ứng dụng cần point read ổn định ở p99 hoặc range scan rộng, cấu hình compaction, cache và key design vẫn quyết định kết quả.
Cache rút ngắn read path nhưng tạo bài toán gì?
Cache biến read path từ “đi đến nguồn sự thật” thành “thử lớp nhanh trước”. Nếu cache hit, độ trễ giảm mạnh và database bớt tải. Nếu cache miss, request vẫn đi toàn bộ đường cũ và còn thêm một lần kiểm tra.
Ba vấn đề phổ biến là:
1. Invalidation
Khi dữ liệu thay đổi, entry cũ phải bị xóa hoặc cập nhật. Nếu quá trình này thất bại, người dùng đọc dữ liệu cũ.
2. Stampede
Khi một key nóng hết hạn, nhiều request cùng lúc truy vấn database để tái tạo cache. Một tối ưu read path có thể biến thành đợt tải đột biến.
3. Hai định nghĩa về consistency
Database consistency có thể nói về invariant và transaction, còn cache consistency thường nói về độ mới giữa cache và nguồn. Hai khái niệm liên quan nhưng không đồng nhất. Một response hợp lệ theo schema vẫn có thể cũ; một cache vừa mới cập nhật vẫn có thể phản ánh giao dịch chưa hoàn tất nếu thiết kế sai ranh giới.
TTL không phải giải pháp hoàn chỉnh. Nó chỉ đặt giới hạn gần đúng cho thời gian dữ liệu cũ tồn tại. Hệ thống cần nói rõ: dữ liệu nào được phép cũ bao lâu, thao tác nào bắt buộc read-your-own-writes và lỗi invalidation được quan sát thế nào.
Read replica có làm đọc miễn phí không?
Read replica tăng khả năng phục vụ truy vấn và tách một phần workload khỏi primary. Tuy nhiên, replication tạo độ trễ giữa lần ghi và thời điểm replica thấy dữ liệu.
Một lỗi trải nghiệm thường gặp:
- người dùng cập nhật hồ sơ trên primary;
- ứng dụng chuyển sang trang chi tiết;
- trang đọc từ replica chưa bắt kịp;
- người dùng thấy dữ liệu cũ và bấm lưu lần nữa.
Cách xử lý có thể là:
- đọc từ primary trong một khoảng ngắn sau ghi;
- dùng session token hoặc LSN để chọn replica đã bắt kịp;
- pin session vào một nguồn đọc;
- hiển thị trạng thái đang đồng bộ;
- thiết kế thao tác ghi idempotent.
Thêm replica là quyết định về consistency và routing, không chỉ là quyết định scale.
Nên đo chỉ số nào cho read path và write path?
Không nên tối ưu theo latency trung bình. Cần xem phân phối và tài nguyên đi kèm.
| Nhóm | Chỉ số nên theo dõi |
|---|---|
| Read path | p50, p95, p99 latency; rows scanned/returned; cache hit; disk read; query plan; replica lag |
| Write path | commit latency; WAL/commit log rate; fsync; lock wait; index update; replication acknowledgement |
| Công việc nền | checkpoint; compaction backlog; vacuum; flush; queue depth; disk utilization |
| Amplification | bytes read/request; bytes written/logical byte; số file hoặc index chạm tới |
| Độ đúng | stale read; conflict; failed invariant; retry; duplicate write |
Một dashboard hữu ích phải nối request của ứng dụng với database spans, query fingerprint và trạng thái storage. Nếu chỉ thấy CPU database tăng, ta biết có vấn đề nhưng chưa biết đường dẫn nào tạo ra nó.
Quy trình 7 bước tối ưu read path và write path
Bước 1: Mô tả workload thật
Ghi lại tỷ lệ đọc ghi, kích thước bản ghi, access pattern, peak traffic, yêu cầu range scan, độ mới cho phép và SLO p95/p99. Không suy đoán chỉ từ tên sản phẩm.
Bước 2: Tách hành trình nóng
Xác định ba đến năm query và command chiếm phần lớn traffic hoặc ảnh hưởng doanh thu. Một hệ thống hiếm khi cần tối ưu đồng đều mọi endpoint.
Bước 3: Đo toàn bộ đường đi
Trace từ API qua cache, database, replica và queue. Ghi cả foreground latency lẫn background debt như compaction hoặc vacuum.
Bước 4: Kiểm tra thiết kế dữ liệu
Xem primary key, partition key, cardinality, kích thước row, index, quan hệ và vòng đời dữ liệu. Nhiều vấn đề hiệu năng bắt đầu từ data model không khớp access pattern.
Bước 5: Chọn nơi trả chi phí
Nếu đọc là hành trình doanh thu và ghi có thể xử lý bất đồng bộ, precompute có thể hợp lý. Nếu ingest là nút thắt và đọc ít, LSM-tree hoặc batch write có thể phù hợp hơn. Ghi rõ consistency budget.
Bước 6: Thử nghiệm với dữ liệu giống production
Benchmark phải có phân phối key, kích thước, concurrency, cache state và thời gian đủ dài để công việc nền xuất hiện. Bài test ngắn với cache nóng thường che giấu compaction và checkpoint.
Bước 7: Triển khai có guardrail
Canary, theo dõi tail latency, error rate, lag, disk, queue và khả năng rollback. Đừng xóa index cũ hoặc đổi routing toàn bộ trước khi biết read path mới ổn định.
Khi nào nên thêm index, cache, replica hay read model?
| Triệu chứng | Lựa chọn đầu tiên nên đánh giá | Cảnh báo |
|---|---|---|
| Query lọc ít hàng nhưng scan lớn | Index phù hợp | Đo chi phí ghi và kích thước index |
| Dữ liệu nóng được đọc lặp lại | Cache | Thiết kế invalidation và chống stampede |
| Primary cạn read capacity | Read replica | Xử lý lag và read-after-write |
| Dashboard join, aggregate nặng | Read model hoặc materialized view | Chấp nhận eventual consistency rõ ràng |
| Ingest throughput thấp | Batch, WAL tuning, LSM design | Không làm xấu p99 read ngoài SLO |
| Nhiều index ít dùng | Loại bỏ có kiểm chứng | Kiểm tra query hiếm nhưng quan trọng |
Không nên nhảy thẳng sang một công nghệ mới trước khi biết giới hạn nằm ở query, schema, cấu hình, I/O hay consistency contract.
Những sai lầm thường gặp
Tối ưu trung bình thay vì tail latency
p50 đẹp không cứu được checkout nếu p99 tăng mạnh lúc checkpoint hoặc compaction.
Thêm index cho mọi truy vấn chậm
Index có thể chữa một query nhưng làm toàn bộ write path chậm hơn. Hãy đo usage, selectivity và maintenance cost.
Xem cache là nguồn sự thật thứ hai
Cache càng mang nhiều business rule và dữ liệu dẫn xuất khó tái tạo, rủi ro sai lệch càng cao.
Bỏ qua thao tác xóa
Trong LSM-tree, xóa thường tạo tombstone. Trong hệ thống event-driven, xóa còn phải lan qua cache, search và analytics. Delete cũng là một write path đầy đủ.
Benchmark khi hệ thống còn “sạch”
Database mới, cache nóng và không có compaction backlog không đại diện production đã chạy nhiều tháng.
FAQ - Câu hỏi thường gặp
Read path và write path có phải chỉ dùng cho database không?
Không. Khái niệm này áp dụng cho cache, search engine, event store, data lake và cả hệ thống AI. Ví dụ một knowledge base có write path gồm ingest, chunk, embedding và index; read path gồm tìm kiếm, rerank và tạo context cho mô hình.
Database nào đọc nhanh nhất?
Không có câu trả lời chung. Kết quả phụ thuộc access pattern, dataset, consistency, phần cứng, cấu hình và SLO. Một engine mạnh ở point lookup có thể không tối ưu cho range scan hoặc ingest lớn.
Thêm nhiều index có luôn làm đọc nhanh hơn không?
Không. Planner có thể không dùng index vì selectivity thấp, thống kê sai hoặc query trả quá nhiều hàng. Mỗi index còn tăng chi phí ghi, lưu trữ và bảo trì.
LSM-tree có luôn ghi nhanh hơn B-tree không?
Không trong mọi điều kiện. LSM-tree thường có lợi thế với ghi tuần tự và throughput cao, nhưng compaction, durability, kích thước value và phần cứng có thể thay đổi kết quả. Cần benchmark workload thật.
Có thể vừa strong consistency vừa đọc ghi cực nhanh không?
Có thể đạt hiệu năng rất tốt trong một phạm vi, nhưng strong consistency giữa nhiều node thường thêm coordination và giới hạn bởi latency mạng, failure mode cùng workload. Cần xác định chính xác thao tác nào cần bảo đảm mạnh.
Tối ưu nào nên làm trước?
Bắt đầu từ query plan, data model, index usage, cache hit, tail latency và tài nguyên. Chỉ thêm tầng kiến trúc sau khi đo được nút thắt và xác định consistency budget.
Tóm lại
Read path và write path là hai mặt của cùng một thiết kế dữ liệu. B-tree, LSM-tree, WAL, MVCC, index, cache, replica và read model không phải những mẹo rời rạc. Chúng là các cách phân phối công việc giữa lúc ghi, lúc đọc và tác vụ nền.
Ba nguyên tắc nên giữ:
- tối ưu theo access pattern và SLO đã đo, không theo nhãn công nghệ;
- tính cả chi phí nền, amplification và consistency vào kết quả;
- mỗi lần rút ngắn read path, hãy hỏi write path và vận hành phải trả thêm gì.
Nếu đang xây workflow nhiều dịch vụ, hãy đọc thêm API Composition là gì? để hiểu cách dữ liệu được ghép ở tầng ứng dụng. Với hệ thống tự động hóa có retry, bài Rủi ro AI Agent trong marketing giúp mở rộng góc nhìn từ hiệu năng sang kiểm soát hành động và dữ liệu.
Nguồn tham khảo
- The Read Path versus the Write Path, ByteByteGo.
- Write-Ahead Logging, PostgreSQL.
- MVCC Introduction, PostgreSQL.
- Storage Engine, Apache Cassandra.
- MultiGet Performance, RocksDB.