Encoding & serialization — text vs binary
Object trong RAM không ghi thẳng xuống đĩa hay gửi qua mạng được. Vì sao cần encode, và đánh đổi text (JSON) dễ đọc vs binary (Avro/Protobuf) gọn nhanh.
TL;DR: Dữ liệu trong bộ nhớ là object — con trỏ, cấu trúc, tham chiếu — chỉ có nghĩa trong tiến trình đang chạy. Để lưu xuống đĩa hay gửi qua mạng, nó phải thành một chuỗi byte tự chứa: đó là encoding (hay serialization). Đọc ngược lại thành object là decoding (deserialization). Có hai họ định dạng: text (JSON, XML, CSV) — người đọc được, phổ biến, nhưng tốn chỗ và schema lỏng lẻo; và binary (Avro, Protocol Buffers, Thrift) — gọn, nhanh parse, có schema chặt, đổi lại không đọc bằng mắt được. Chọn loại nào tuỳ bạn cần con người đọc hay máy xử lý khối lượng lớn. Đây là quyết định nền cho cả storage lẫn truyền dữ liệu.
Một service gọi API trả về một User object: có id, name, một list roles, và một con trỏ tới manager (cũng là User). Bạn muốn gửi nó cho service khác qua mạng. Nhưng "con trỏ tới manager" là một địa chỉ bộ nhớ — vô nghĩa với tiến trình khác, trên máy khác. Bạn không thể memcpy object qua mạng và mong nó hoạt động.
Đây là vấn đề nền tảng: biểu diễn trong bộ nhớ và biểu diễn để lưu/truyền là hai thứ khác nhau. Bài này giải thích vì sao cần encode, hai họ định dạng để encode, và cách chọn — kiến thức áp dụng cho mọi lúc dữ liệu rời khỏi RAM: ghi file, gọi API, đẩy vào message queue, lưu database.
1. Analogy — Đóng gói đồ chuyển nhà
Đồ đạc trong nhà bạn được sắp xếp tiện cho sống: tủ lạnh cắm điện, sách trên kệ theo chủ đề, dây nối chằng chịt. Nhưng để chuyển sang nhà khác, bạn không bê nguyên cả căn phòng lên xe — bạn đóng gói: tháo rời, gói vào thùng có nhãn, xếp gọn. Đến nơi, bạn mở gói và lắp lại.
- Đóng gói (serialize): chuyển trạng thái "đang sống, có liên kết" thành dạng "phẳng, tự chứa, vận chuyển được".
- Nhãn thùng (schema): cho biết bên trong là gì để lắp lại đúng.
- Mở gói (deserialize): dựng lại trạng thái sống ở đầu bên kia.
Object trong RAM giống căn phòng đang sống (có con trỏ, liên kết). Chuỗi byte để lưu/truyền giống thùng đồ đã đóng gói. Bạn không thể "chuyển nhà" mà bỏ qua bước đóng gói.
flowchart LR
OBJ["Object trong RAM<br/>(con tro, lien ket)"] -->|"encode<br/>(serialize)"| BYTES["Chuoi byte<br/>phang, tu chua"]
BYTES -->|"luu / truyen"| MED[("Dia hoac mang")]
MED --> BYTES2["Chuoi byte"]
BYTES2 -->|"decode<br/>(deserialize)"| OBJ2["Object dung lai<br/>o tien trinh khac"]| Chuyển nhà | Encoding dữ liệu |
|---|---|
| Đồ sắp xếp để sống (cắm điện, liên kết) | Object trong RAM (con trỏ, tham chiếu) |
| Đóng gói vào thùng phẳng | Serialize → chuỗi byte tự chứa |
| Nhãn thùng "Sách — phòng khách" | Schema (cấu trúc + kiểu field) |
| Mở gói, lắp lại | Deserialize → dựng lại object |
| Thùng to chiếm chỗ xe | Encoding tốn nhiều byte |
Encode = đóng gói object thành byte phẳng tự chứa để lưu/truyền. Decode = mở gói dựng lại object. Con trỏ trong RAM không đi xa được — phải đóng gói trước.
2. Vì sao object không thể lưu/truyền trực tiếp
Trong bộ nhớ, một object là một mạng lưới: các trường nằm rải rác, nối nhau bằng con trỏ (địa chỉ bộ nhớ). Hai lý do con trỏ không vượt ra ngoài tiến trình:
- Địa chỉ chỉ có nghĩa cục bộ. Con trỏ
0x7ffe...trỏ tới một ô nhớ trong tiến trình này. Tiến trình khác (hay máy khác) có bố cục bộ nhớ hoàn toàn khác — địa chỉ đó trỏ vào hư không. - Bố cục phụ thuộc runtime. Cách một ngôn ngữ/runtime sắp object trong RAM (padding, thứ tự field, header) là chi tiết nội bộ, không ổn định giữa phiên bản, máy, hay ngôn ngữ.
Trong RAM (tien trinh A):
user --> [id|0x40][name|0x88][roles|0x12c]
0x88 --> "Mai" (con tro -> chuoi)
0x12c --> [list node...] (con tro -> list)
Gui 0x88 sang tien trinh B? -> B doc 0x88 cua chinh no -> rac
Encoding giải bài này bằng cách thay con trỏ bằng nội dung thực, sắp mọi thứ thành một chuỗi byte tuyến tính, tự chứa — đọc đâu cũng đủ, không cần tra địa chỉ ở nơi khác. Đó là điều kiện để byte đó sống độc lập trên đĩa hay đường truyền.
3. Text formats — JSON, XML, CSV
Họ thứ nhất encode dữ liệu thành văn bản con người đọc được. Bạn mở file bằng editor là thấy nội dung.
{
"id": 42,
"name": "Mai",
"roles": ["admin", "editor"],
"manager_id": 7
}
Đặc điểm:
- Ưu — đọc được & gỡ lỗi dễ: mở ra xem ngay, debug bằng mắt, mọi ngôn ngữ đều có thư viện parse. JSON gần như là lingua franca của API web.
- Ưu — không cần schema để đọc: byte tự mô tả tên field ngay trong dữ liệu (
"name": ...). - Nhược — tốn chỗ: tên field lặp lại trong mọi bản ghi. Một triệu user thì chữ
"name"xuất hiện một triệu lần. Số cũng lưu dạng chuỗi ký tự, không nén. - Nhược — kiểu lỏng lẻo: JSON không phân biệt số nguyên với số thực, không có kiểu ngày tháng, dễ mơ hồ. CSV còn tệ hơn: mọi thứ là chuỗi, không có kiểu, ranh giới cột dễ vỡ khi giá trị chứa dấu phẩy.
CSV hợp cho bảng phẳng đơn giản (xuất ra Excel); XML dài dòng hơn JSON, nay ít dùng cho dữ liệu mới. Điểm chung của cả họ: người đọc được, nhưng trả giá bằng kích thước và sự thiếu chặt chẽ về kiểu.
4. Binary formats — Avro, Protocol Buffers, Thrift
Họ thứ hai encode thành byte máy đọc, không phải văn bản. Mở bằng editor chỉ thấy ký tự lạ. Đổi lại: gọn hơn nhiều và có schema chặt.
Ý tưởng cốt lõi: thay vì nhúng tên field vào mỗi bản ghi (như JSON), binary format tách schema (mô tả cấu trúc một lần) khỏi dữ liệu (chỉ chứa giá trị). Một số format dùng field tag — mỗi field gắn một số nguyên nhỏ thay cho tên chuỗi:
Schema (dinh nghia 1 lan, agnostic):
message User {
int32 id = 1; -- field tag 1
string name = 2; -- field tag 2
repeated role roles = 3; -- field tag 3
}
Du lieu tren day (khai niem): khong co ten field, chi co
tag=1 -> 42, tag=2 -> "Mai", tag=3 -> [admin, editor]
Vì không lặp tên field và mã hoá số ở dạng nhị phân, bản ghi gọn hơn JSON đáng kể. Parse cũng nhanh hơn vì máy nhảy theo tag thay vì quét chuỗi tìm "name".
Một khác biệt đáng nhớ giữa các format: schema để ở đâu:
- Protocol Buffers / Thrift: field tag được nhúng trong dữ liệu; bên đọc cần file schema (
.proto) để biết tag nào nghĩa gì. - Avro: dữ liệu không chứa tag, chỉ là giá trị theo thứ tự — nên bên đọc cần cả schema ghi lẫn schema đọc để khớp. Đổi lại Avro rất gọn và linh hoạt với schema thay đổi (chủ đề bài 03).
Việc dùng số tag thay cho tên field không chỉ để gọn. Nó là nền cho schema evolution: tag 2 luôn là name dù bạn đổi tên hiển thị, thêm hay bớt field khác. Bên đọc cũ gặp tag lạ thì bỏ qua; bên đọc mới thiếu tag thì dùng giá trị mặc định. Bài 03 mổ kỹ cơ chế này.
5. So sánh & khi nào dùng cái nào
| Tiêu chí | Text (JSON/CSV/XML) | Binary (Avro/Protobuf/Thrift) |
|---|---|---|
| Con người đọc được | Có | Không (cần tool) |
| Kích thước | Lớn (lặp tên field) | Nhỏ (tag số + nén nhị phân) |
| Tốc độ parse | Chậm hơn | Nhanh hơn |
| Schema | Lỏng / ngầm | Chặt / tường minh |
| Cần schema để đọc | Không | Có (file schema) |
| Hỗ trợ tiến hoá schema | Yếu, thủ công | Mạnh, có quy tắc |
| Hợp cho | API public, config, gỡ lỗi | RPC nội bộ, message khối lớn, lưu trữ dài hạn |
Quy tắc chọn:
- Chọn text khi con người là một bên đọc: API công khai, file cấu hình, log để mắt người xem, dữ liệu trao đổi với đối tác cần dễ kiểm. Sự tiện gỡ lỗi đáng giá hơn vài byte.
- Chọn binary khi máy xử lý khối lượng lớn: giao tiếp giữa các service nội bộ (RPC), event đẩy hàng triệu/giây vào message queue, hay lưu trữ dài hạn nơi mỗi byte nhân lên hàng tỉ lần. Gọn + nhanh + schema chặt thắng thế.
Quyet dinh nhanh:
Nguoi se doc byte nay? -> TEXT (JSON)
May xu ly khoi lon, can gon/nhanh? -> BINARY (Avro/Protobuf)
6. Pitfall — nhầm "đọc được" với "đúng"
Vì JSON/CSV không ép kiểu chặt, lỗi không nổ lúc ghi mà nổ về sau, ở chỗ khác:
- Số thành chuỗi: một bên ghi
"id": 42, bên khác ghi"id": "42". JSON nhận cả hai; code đọc cộng số thì một dòng raNaN. - CSV vỡ cột: một giá trị chứa dấu phẩy không escape (
"Hanoi, Vietnam") đẩy lệch mọi cột sau nó — không lỗi, chỉ dữ liệu sai âm thầm. - Thiếu/thừa field không ai chặn: text không có ai kiểm "bản ghi này có đủ field bắt buộc không".
WRONG (tin tuong text la du):
doc CSV -> gia su cot 3 luon la so -> mot hang co dau phay -> lech cot -> sai
RIGHT (ep kiem):
- dung binary co schema (parse fail neu sai kieu/thieu field), HOAC
- validate JSON theo schema (vd JSON Schema) ngay luc nhap
"Đọc được bằng mắt" không bằng "đúng về cấu trúc". Khi dữ liệu đi giữa nhiều service hay sống lâu, sự chặt chẽ của schema (binary, hoặc text + validation) quan trọng hơn sự tiện đọc.
7. 📚 Deep Dive
- Designing Data-Intensive Applications (Kleppmann) — Chương 4 "Encoding and Evolution" — nguồn nền tảng: vì sao biểu diễn in-memory khác biểu diễn để lưu/truyền, và so sánh chi tiết JSON/XML/CSV với Thrift/Protobuf/Avro.
- Protocol Buffers — tài liệu chính thức (Google) — "language-neutral, platform-neutral, extensible mechanism for serializing structured data". Xem khái niệm field number (tag) và message definition.
- Apache Avro — tài liệu chính thức — "a data serialization system"; điểm đặc trưng: dữ liệu không nhúng tag, dựa vào schema khi đọc.
Ghi chú: Cả ba format binary trên đều agnostic — là chuẩn mở, không gắn database engine nào. DDIA Chương 4 là nơi đọc để hiểu vì sao binary gọn hơn và vì sao field tag mở đường cho tiến hoá schema. Đọc tiếp bài 03 để thấy tag biến thành công cụ đổi schema không downtime.
8. Liên hệ các bài khác
- Bài 01 — Vòng đời dữ liệu: encode là việc xảy ra ở mọi tầng storage và serving — quyết định text/binary ảnh hưởng chi phí lưu ở tầng cold và tốc độ truyền ở serving.
- Bài 03 — Schema evolution: field tag và schema của binary format chính là nền cho khả năng đổi schema mà không vỡ dữ liệu/client cũ.
- Module 10 — OLTP vs OLAP: kho phân tích thường lưu dữ liệu dạng binary cột (column-oriented) — đẩy ý "gọn + nhanh quét" của binary tới cực hạn.
9. Tóm tắt
- Biểu diễn dữ liệu trong RAM (object, con trỏ) khác biểu diễn để lưu/truyền (chuỗi byte tự chứa) — con trỏ chỉ có nghĩa cục bộ nên không vượt ra ngoài tiến trình.
- Encoding/serialization chuyển object thành byte tuyến tính tự chứa; decoding dựng lại object.
- Text (JSON/XML/CSV): người đọc được, phổ biến, không cần schema để đọc — nhưng tốn chỗ (lặp tên field) và kiểu lỏng lẻo.
- Binary (Avro/Protobuf/Thrift): gọn, parse nhanh, có schema chặt với field tag số — đổi lại không đọc bằng mắt, cần file schema.
- Khác biệt schema-ở-đâu: Protobuf/Thrift nhúng tag trong dữ liệu; Avro dựa hoàn toàn vào schema khi đọc.
- Chọn text khi con người là một bên đọc (API public, config); chọn binary khi máy xử lý khối lớn (RPC, message queue, lưu trữ dài hạn).
- Pitfall: "đọc được" không bằng "đúng cấu trúc" — schema lỏng của text âm thầm hỏng dữ liệu; dùng binary hoặc validate text khi dữ liệu sống lâu/đi xa.
10. Tự kiểm tra
Q1Vì sao không thể gửi thẳng một object trong RAM (kèm con trỏ tới object khác) qua mạng tới tiến trình khác?▸
Vì con trỏ chỉ có nghĩa cục bộ. Một con trỏ là địa chỉ một ô nhớ trong tiến trình hiện tại; tiến trình khác (hay máy khác) có bố cục bộ nhớ hoàn toàn khác, nên cùng địa chỉ đó trỏ vào dữ liệu rác.
Thêm nữa, cách runtime sắp object trong RAM (padding, thứ tự field, header) là chi tiết nội bộ không ổn định giữa máy/ngôn ngữ. Vì vậy phải encode: thay con trỏ bằng nội dung thực và sắp mọi thứ thành chuỗi byte tuyến tính, tự chứa — đọc đâu cũng đủ, không cần tra địa chỉ ở nơi khác.
Q2JSON lưu một triệu bản ghi user tốn nhiều chỗ hơn binary format ở điểm nào cụ thể?▸
Hai chỗ chính. Thứ nhất, JSON nhúng tên field vào mọi bản ghi — chuỗi "name", "roles" lặp lại một triệu lần dù cấu trúc giống hệt nhau. Binary tách schema ra ngoài (định nghĩa một lần) và chỉ lưu giá trị, thường gắn một số tag nhỏ thay cho tên chuỗi.
Thứ hai, JSON lưu số dưới dạng chuỗi ký tự (số 1000000 là 7 byte ký tự), còn binary mã hoá số ở dạng nhị phân gọn hơn. Cộng lại, binary nhỏ hơn đáng kể và parse cũng nhanh hơn vì máy nhảy theo tag thay vì quét chuỗi tìm tên field.
Q3Một team xây API công khai cho lập trình viên bên ngoài tích hợp. Nên chọn text hay binary, vì sao?▸
Nên chọn text (thường là JSON). Lý do: bên tích hợp là con người ở tổ chức khác — họ cần đọc response bằng mắt khi gỡ lỗi, mọi ngôn ngữ đều có sẵn thư viện JSON, và họ không có sẵn file schema binary của bạn.
Sự tiện đọc và phổ quát đáng giá hơn vài byte tiết kiệm. Binary hợp khi cả hai đầu do bạn kiểm soát (service nội bộ) và khối lượng đủ lớn để kích thước/tốc độ thành vấn đề. API public thì rào cản tích hợp quan trọng hơn — nên text thắng.
Q4Avro và Protocol Buffers khác nhau ở chỗ 'schema để ở đâu' như thế nào, và hệ quả là gì?▸
Protocol Buffers (và Thrift) nhúng field tag (số) ngay trong dữ liệu — bên đọc cần file schema để biết tag nào nghĩa gì, nhưng dữ liệu tự đánh dấu từng field.
Avro không nhúng tag: dữ liệu chỉ là các giá trị theo thứ tự, nên bên đọc cần cả schema lúc ghi lẫn schema lúc đọc để khớp đúng. Hệ quả: Avro gọn hơn nữa (không tốn byte cho tag) và linh hoạt với schema thay đổi, nhưng phụ thuộc chặt vào việc quản lý schema đi kèm dữ liệu. Cả hai cách đều phục vụ cùng mục tiêu: tách schema khỏi dữ liệu để gọn và để tiến hoá được.
Q5Vì sao 'dữ liệu đọc được bằng mắt' (text) không đảm bảo dữ liệu 'đúng về cấu trúc'? Cho một cách phòng.▸
Vì text format có kiểu lỏng lẻo, không ai ép. JSON nhận cả "id": 42 lẫn "id": "42" (số vs chuỗi) mà không báo lỗi; CSV có một giá trị chứa dấu phẩy không escape sẽ đẩy lệch mọi cột sau mà vẫn parse "thành công". Lỗi không nổ lúc ghi mà nổ về sau, ở chỗ khác — khó truy.
Cách phòng: dùng binary có schema (parse thất bại ngay nếu sai kiểu hay thiếu field bắt buộc), hoặc nếu vẫn dùng text thì validate theo schema (ví dụ JSON Schema) ngay tại điểm nhập. Khi dữ liệu đi giữa nhiều service hoặc sống lâu, sự chặt chẽ quan trọng hơn sự tiện đọc.
Bài tiếp theo: Schema evolution — đổi schema không downtime
Bài này có giúp bạn hiểu bản chất không?
Hỏi đáp về bài này
Chưa có câu hỏi
Có gì chưa rõ trong bài? Đặt câu hỏi đầu tiên — câu trả lời từ cộng đồng giúp bạn (và người sau).
Đặt câu hỏi đầu tiên