Một tờ ghi nhớ dán trên màn hình và một kho lưu trữ có mục lục cùng "chứa thông tin", nhưng so xem cái nào nhanh hơn là hỏi sai câu. Tờ ghi nhớ trả lời tức thì đúng một câu hỏi — "cái tôi dán ở đây là gì" — và mất sạch khi ai đó dọn bàn. Kho lưu trữ chậm hơn mỗi lần tra, nhưng trả lời được "hồ sơ nào có chữ này", "những cái lập tháng trước", và không mất khi mất điện. Redis so với PostgreSQL đúng là tờ ghi nhớ so với kho lưu trữ, và bài này mở đầu sê-ri bằng cách đo cả hai trên đúng cùng một bài toán thay vì tin lời quảng cáo.
Cùng một bài toán, 50 kết nối
Bài toán đơn giản nhất có thể: đọc và ghi khoá–giá trị, giá trị 100 byte, 100.000 khoá.
| Thông lượng | Độ trễ | |
|---|---|---|
Redis SET |
316.456/s | 0,079 ms |
Redis GET |
331.126/s | 0,079 ms |
PostgreSQL INSERT (có WAL) |
77.187/s | 0,648 ms |
PostgreSQL INSERT (unlogged) |
227.510/s | 0,220 ms |
PostgreSQL SELECT |
169.360/s | 0,295 ms |
Redis nhanh hơn 4,1 lần khi ghi và 2,0 lần khi đọc. Không phải 100 lần. Con số đó xuất hiện khắp nơi và tôi vào bài này trông đợi thấy một khoảng cách lớn hơn nhiều.
Phần lớn khoảng cách là độ bền, không phải kiến trúc
Dòng thứ tư là dòng quan trọng nhất. Cùng PostgreSQL, cùng truy vấn, chỉ khác một chữ UNLOGGED trên bảng:
có WAL 77.187/s
không WAL 227.510/s
Bỏ ghi nhật ký trước là gấp 2,9 lần, và khoảng cách với Redis co lại còn 1,4 lần. Điều này đổi hẳn cách nên nghĩ về so sánh. Redis nhanh không chủ yếu vì nó "trong bộ nhớ" — PostgreSQL cũng phục vụ phần lớn từ bộ đệm trong bộ nhớ. Redis nhanh chủ yếu vì mặc định nó không hứa gì về độ bền.
Đó là một đánh đổi, không phải một phép màu. Và khi bạn bật độ bền của Redis lên mức tương đương WAL — appendfsync always — thì đánh đổi đó quay lại, phần sau của sê-ri sẽ đo.
Bộ nhớ mỗi bản ghi: gần bằng nhau
Redis 185 byte mỗi khoá (dữ liệu thật ~100 byte)
PostgreSQL 17 MB / 99.996 dòng = ~178 byte mỗi dòng
Tôi trông đợi Redis gọn hơn nhiều. Nó không. 185 byte cho 100 byte dữ liệu là 85% phụ trội — cho con trỏ, bảng băm, tiêu đề đối tượng, và bộ cấp phát. PostgreSQL 178 byte đã gồm cả chỉ mục khoá chính.
Khác biệt thật không nằm ở kích thước mà ở chỗ nằm: 185 byte của Redis phải ở trong RAM, toàn bộ, mọi lúc. 178 byte của PostgreSQL nằm trên đĩa, và chỉ phần đang dùng mới ở trong RAM. Với 10 triệu bản ghi, đó là 1,85 GB RAM bắt buộc so với 1,78 GB đĩa. Với 1 tỷ bản ghi thì một bên là bất khả thi còn bên kia là một ổ đĩa bình thường.
Chỗ Redis không làm được
Tôi đặt một giá trị đặc biệt vào một khoá rồi đi tìm nó bằng giá trị.
PostgreSQL, không chỉ mục 6,686 ms
PostgreSQL, có chỉ mục 0,573 ms
Redis, quét 63.000 khoá 20 giây vẫn chưa xong
Redis chỉ biết trả lời một câu hỏi: "khoá này có gì" — đúng tờ ghi nhớ. Mọi câu hỏi khác — tìm theo giá trị, lọc theo khoảng, sắp xếp, nối bảng — nó không có khái niệm. Cách làm trong Redis là tự dựng cấu trúc để trả lời trước: nếu cần tìm theo giá trị thì bạn phải tự tạo thêm một khoá ngược, tự cập nhật nó mỗi lần ghi, và tự lo chuyện hai khoá lệch nhau khi có lỗi giữa chừng. Trong PostgreSQL, việc đó là CREATE INDEX — một dòng, và cơ sở dữ liệu tự lo phần nhất quán.
Đây là chỗ so sánh "nhanh hơn bao nhiêu lần" trở nên vô nghĩa: hai hệ thống này không làm cùng một việc.
Vậy Redis là gì
Từ ba phép đo trên, một định nghĩa thực dụng: Redis là một bảng băm rất nhanh, sống trong RAM, có sẵn mạng và một số kiểu dữ liệu dựng sẵn. Nó thắng rõ khi:
- Bạn biết chính xác khoá cần đọc. Không tìm kiếm, không lọc.
- Dữ liệu vừa trong RAM và bạn chấp nhận trả tiền cho RAM đó.
- Mất một phần dữ liệu khi sự cố là chấp nhận được — bộ đệm, phiên đăng nhập, bộ đếm tần suất.
- Bạn cần đúng những thứ nó dựng sẵn: hàng đợi, tập hợp, bảng xếp hạng, khoá phân tán, luồng sự kiện.
Bốn phần sau của sê-ri sẽ đo từng thứ trong danh sách cuối. Nó không phải:
- Nơi lưu dữ liệu chính, trừ khi bạn đã đo và chấp nhận mô hình bền của nó.
- Một cơ sở dữ liệu để truy vấn.
- Cách rẻ để đi nhanh — RAM đắt hơn đĩa nhiều lần, và đo được ở trên là mỗi bản ghi tốn xấp xỉ nhau.
Sai lầm phổ biến nhất
Không phải "dùng Redis khi không cần". Mà là dùng Redis rồi quên mất nó không bền. Bộ đệm bị mất thì ứng dụng chậm đi một lúc — đó là thiết kế đúng. Nhưng nếu ứng dụng của bạn ghi cái gì đó chỉ vào Redis, thì một lần khởi động lại là mất thật, và không có bản sao ở đâu để đối chiếu.
Câu hỏi kiểm tra nhanh cho mọi thứ bạn đang cất trong Redis: "Nếu toàn bộ Redis biến mất ngay bây giờ, cái gì không dựng lại được?" Mọi câu trả lời khác "không có gì" đều đáng xem lại.
Mẫu số chung: "nhanh hơn X lần" vô nghĩa khi hai bên hứa những điều khác nhau
Cái bẫy "Redis nhanh hơn 100 lần" là một dạng so sánh sai mà ta gặp ở khắp nơi: đặt hai công cụ cạnh nhau trên một trục (thông lượng) mà quên rằng chúng đang hứa những điều khác nhau. Phần lớn khoảng cách 4,1 lần biến mất khi cho PostgreSQL bỏ độ bền (UNLOGGED) — nghĩa là ta đang đo cái giá của một đảm bảo, không phải của một kiến trúc. Và phần còn lại của "chênh lệch" thực ra là chúng trả lời những câu hỏi khác nhau: một cái tra theo khoá, một cái truy vấn mọi chiều.
Trong thế giới Java và hệ thống nói chung, đây đúng là chuyện so một cache trong bộ nhớ (Caffeine, một ConcurrentHashMap) với một CSDL rồi kết luận cache "nhanh hơn nghìn lần" — đúng, nhưng cache không bền, không truy vấn được, không nhất quán giữa các nút; bạn đang trả các đảm bảo đó để lấy tốc độ. Cũng là cái bẫy của mọi benchmark NoSQL-so-SQL chạy ở mức cô lập hay độ bền khác nhau: fsync bật hay tắt, synchronous_commit on hay off, quorum hay eventual — đổi một cờ là đảo cả bảng xếp hạng.
Mẫu số chung: "nhanh hơn X lần" chỉ có nghĩa khi hai bên cho cùng một đảm bảo và trả lời cùng một câu hỏi — phần lớn khoảng cách tốc độ giữa các kho dữ liệu là cái giá của độ bền/nhất quán bị bỏ, không phải phép màu kiến trúc. Câu hỏi đúng khi chọn không phải "cái nào nhanh hơn" mà là "tôi cần đảm bảo gì (bền? truy vấn được? nhất quán?), và với cùng bộ đảm bảo đó thì chênh lệch còn bao nhiêu" — vì đó mới là con số thật. Với Redis, câu trả lời gần như luôn là: dùng nó cho đúng việc nó làm (tra theo khoá, chấp nhận mất), đừng dùng nó vì nó "nhanh".
Muốn tự xem Redis của bạn đang giữ gì và tốn bao nhiêu cho mỗi khoá:
redis-cli info memory | grep -E 'used_memory_human|maxmemory_human|used_memory_rss_human'
redis-cli dbsize
redis-cli --bigkeys
--bigkeys quét toàn bộ và in ra khoá lớn nhất của mỗi kiểu. Nó là cách nhanh nhất tìm ra thứ đang ăn RAM mà bạn không nhớ đã đặt vào — thường là một danh sách không ai xoá, hoặc một tập hợp lớn dần theo thời gian. Và nếu maxmemory là 0, tức chưa đặt giới hạn, thì Redis sẽ dùng tới lúc hệ điều hành giết nó. Phần sau đo đúng chuyện đó.
Phần tiếp theo: một luồng mà nhanh — đo xem nút thắt thật của Redis nằm ở đâu.