Phần trước đo được phụ trội 64 byte cho mỗi khoá chuỗi. Bài này đo bốn kiểu tập hợp — và tìm ra ba vách đá bộ nhớ rơi ở đúng một phần tử.
Gom vào một khoá rẻ hơn hẳn
100 phần tử, mỗi phần tử 20 byte:
| Kiểu | Bộ nhớ | Mỗi phần tử |
|---|---|---|
| Hash | 3.120 | 31 byte |
| Sorted Set | 3.120 | 31 byte |
| List | 2.608 | 26 byte |
| Set | 2.608 | 26 byte |
| 100 chuỗi riêng lẻ | 7.200 | 72 byte |
Gom lại tiết kiệm 2,3 đến 2,8 lần, vì phụ trội 64 byte mỗi khoá biến mất — chỉ còn một khoá thay vì một trăm.
Đây là lý do lời khuyên "dùng Hash thay cho nhiều khoá user:1:name, user:1:email" không phải chuyện phong cách mà là chuyện hoá đơn RAM.
Nhưng có vách đá
Redis lưu tập hợp nhỏ dưới dạng listpack — một mảng byte liền mạch, gọn nhưng tra cứu tuyến tính. Vượt ngưỡng, nó chuyển sang cấu trúc thật.
| Trước ngưỡng | Sau ngưỡng | Tỷ lệ | |
|---|---|---|---|
| Hash 512 → 513 trường | 28,1 byte/trường | 72,2 | 2,6× |
| Set 128 → 129 phần tử | 28,4 byte/phần tử | 64,7 | 2,3× |
| Set số nguyên 512 → 513 | 2,6 byte/phần tử | 48,2 | 18,6× |
Dòng cuối là chỗ dốc nhất tôi từng đo trong Redis. Một tập 512 số nguyên dùng mã hoá intset — một mảng số đã sắp xếp, 2,6 byte mỗi số. Thêm đúng một phần tử là nó thành bảng băm và mỗi phần tử tốn 48,2 byte.
Một phần tử, gấp 18,6 lần bộ nhớ.
Ba ngưỡng mặc định đáng thuộc:
hash-max-listpack-entries 512
set-max-listpack-entries 128
set-max-intset-entries 512
zset-max-listpack-entries 128
list-max-listpack-size 128
Kèm theo ngưỡng độ dài giá trị (*-max-listpack-value, mặc định 64 byte) — vượt cái nào cũng chuyển.
Nâng ngưỡng có đáng không
Nâng được, và với dữ liệu nhiều tập hợp nhỏ thì rất đáng:
redis-cli config set hash-max-listpack-entries 1024
Đổi lại: listpack tra cứu tuyến tính. Với 128 phần tử thì quét tuyến tính vẫn nhanh hơn băm vì dữ liệu liền kề và vừa trong bộ nhớ đệm CPU. Với 5.000 phần tử thì không.
Ranh giới thực dụng: nâng lên khoảng 1.000 thường an toàn nếu bạn chủ yếu đọc cả tập (HGETALL, SMEMBERS). Nếu bạn tra từng trường một trong một tập lớn, để nguyên mặc định.
Và đây là chỗ dễ mắc lỗi im lặng: đổi cấu hình không chuyển đổi các khoá đã tồn tại. Chúng giữ mã hoá cũ cho tới khi bị ghi lại. OBJECT ENCODING là cách duy nhất biết chắc.
LINDEX có hình chữ V
List 200.000 phần tử, mã hoá quicklist. Tôi đo LINDEX ở ba vị trí, ba lần:
| Đầu | Giữa | Cuối | |
|---|---|---|---|
| lần 1 | 0,212 µs | 0,297 µs | 0,213 µs |
| lần 2 | 0,210 µs | 0,325 µs | 0,239 µs |
| lần 3 | 0,242 µs | 0,313 µs | 0,237 µs |
Chậm nhất ở giữa, không phải ở cuối.
Tài liệu ghi LINDEX là O(N), và điều đó đúng nhưng thiếu: quicklist là danh sách hai chiều, nên nó đi từ đầu gần nhất. Chi phí thật là O(min(i, N−i)).
Hệ quả thực dụng: dùng list làm hàng đợi thì hoàn toàn ổn — LPUSH/RPOP luôn chạm vào hai đầu. Dùng list làm mảng truy cập ngẫu nhiên thì sai kiểu dữ liệu, và phần tử ở giữa là chỗ đau nhất.
Con số tuyệt đối cũng đáng nhớ: 0,3 µs cho một list 200.000 phần tử. "O(N) chậm" trong Redis vẫn nhanh hơn phần lớn thứ khác — nhưng nhân nó với 10.000 lệnh mỗi giây thì nó thành 3 mili giây CPU, trên một luồng duy nhất mà mọi khách hàng khác đang chờ.
Chọn kiểu nào
| Câu hỏi cần trả lời | Kiểu |
|---|---|
| Đối tượng có nhiều trường, đọc từng trường | Hash |
| Hàng đợi, nhật ký gần đây, chỉ chạm hai đầu | List |
| "Phần tử này có trong tập không" | Set |
| Xếp hạng, khoảng theo điểm, hàng đợi ưu tiên | Sorted Set |
| Đếm số phần tử khác nhau, chấp nhận sai số | HyperLogLog |
Sorted Set là kiểu mạnh nhất và đắt nhất — nó giữ cả bảng băm lẫn danh sách bỏ qua. Dùng nó khi thật sự cần thứ tự; nếu chỉ cần kiểm tra thành viên thì Set rẻ hơn.
Và với Set toàn số nguyên dưới 512 phần tử, intset là cấu trúc rẻ nhất trong toàn bộ Redis — 2,6 byte mỗi phần tử. Nếu id của bạn là số, hãy lưu chúng dưới dạng số.
Thử ba mươi giây
Tìm khoá nào vừa vượt ngưỡng mã hoá — tức là khoá vừa đắt lên gấp đôi mà không ai biết:
redis-cli --scan --count 500 | head -2000 | while read k; do
t=$(redis-cli type "$k")
e=$(redis-cli object encoding "$k")
case "$e" in
hashtable|skiplist|quicklist)
n=$(case "$t" in
hash) redis-cli hlen "$k";;
set) redis-cli scard "$k";;
zset) redis-cli zcard "$k";;
list) redis-cli llen "$k";;
esac)
[ "${n:-0}" -lt 2000 ] && echo "$n phan tu $e $k"
;;
esac
done | sort -n | head -20
Khoá nào dùng mã hoá "lớn" mà chỉ có vài trăm phần tử là ứng viên tốt để nâng ngưỡng — nó đang trả giá của cấu trúc lớn mà không dùng tới lợi ích.
Phần sau: TTL và chính sách đuổi khoá — đo xem Redis xoá khoá hết hạn lúc nào, và cái gì xảy ra khi đầy bộ nhớ.