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ử.

So sánh bộ nhớ bốn kiểu, ba ngưỡng mã hoá, và hình chữ V của LINDEX

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ớ.