Đặt TTL là thao tác phổ biến nhất trong Redis sau GETSET. Bài này đo ba thứ ít ai kiểm: Redis xoá khoá hết hạn lúc nào, lệnh nào làm mất TTL, và TTL tốn bao nhiêu byte.

Kết quả đo tốc độ dọn khoá, bảng lệnh giữ hay mất TTL, và chi phí bộ nhớ

Redis dọn nhanh hơn lời đồn

Tôi tạo 100.000 khoá cùng hết hạn sau 3 giây rồi không đụng vào chúng nữa:

t = 4s   dbsize 0   expired_keys 100.000   bộ nhớ đã trả về

Sạch trong dưới một giây sau thời điểm hết hạn.

Trường hợp khó hơn: 2.000 khoá hết hạn lẫn giữa 1.000.000 khoá sống lâu.

t = 4s   đã xoá 2.000 / 2.000

Cũng dưới một giây.

Tôi vào phép đo này trông đợi thấy khoá hết hạn nằm lại — đó là điều "ai cũng biết" về Redis. Không thấy.

Lý do nằm ở cấu trúc: Redis giữ một bảng expires riêng chỉ chứa khoá có TTL. Vòng quét chủ động lấy mẫu từ bảng đó, nên một triệu khoá không hết hạn hoàn toàn không làm nó chậm lại. Và vòng quét thích nghi: nếu trên 25% mẫu là khoá đã hết hạn, nó lặp lại ngay thay vì chờ chu kỳ sau.

Lời đồn không hoàn toàn sai — nó chỉ thuộc về một tình huống khác: bản sao không tự xoá khoá hết hạn mà chờ lệnh DEL từ bản chính, nên có thể lệch tạm thời. Tôi không đo chuyện đó ở đây, nên đừng lấy con số trên áp cho bản sao.

Lệnh nào giữ TTL, lệnh nào xoá mất

Đặt TTL 100 giây rồi chạy từng lệnh:

Lệnh TTL còn lại
SET k v2 −1 mất
GETSET k v2 −1 mất
GETEX k PERSIST −1 mất (đúng như tên)
SET k v2 KEEPTTL 100 giữ
APPEND k xx 100 giữ
SETRANGE k 0 z 100 giữ
INCRBY k 1 100 giữ
GETEX k 100 giữ

Dòng đáng nhớ nhất: SET xoá TTL nhưng APPEND thì không, dù cả hai đều sửa giá trị.

Quy tắc thật là: lệnh thay thế cả khoá thì xoá TTL; lệnh sửa đổi giá trị đang có thì giữ. SET tạo một đối tượng mới nên nó là thay thế; APPENDINCRBY sửa tại chỗ.

Đây là nguồn của một lớp lỗi rất khó thấy: phiên đăng nhập có TTL 30 phút, đến khi có mã nào đó gọi SET để cập nhật thông tin phiên, TTL biến mất và phiên đó sống mãi. Không có lỗi nào, không có cảnh báo nào — chỉ có một khoá không bao giờ tự dọn.

Cách chữa là thêm KEEPTTL vào mọi SET ghi đè lên khoá có hạn:

SET phien:abc '{"user":123}' KEEPTTL

RENAME mang TTL của nguồn

rename khoá-có-TTL -> đích              đích có TTL 100
rename khoá-không-TTL -> đích-có-TTL    đích mất TTL (−1)

RENAME không hợp nhất mà thay thế: khoá đích nhận toàn bộ trạng thái của nguồn, kể cả việc nguồn không có TTL.

Mẫu "dựng khoá mới rồi RENAME đè lên khoá cũ" — cách phổ biến để cập nhật nguyên tử — sẽ xoá TTL nếu bạn quên đặt TTL cho khoá tạm.

Tương tự với tập hợp: xoá trường cuối cùng của một Hash làm cả khoá biến mất, và TTL đi theo. Nếu logic của bạn dựa vào việc khoá tồn tại tới lúc hết hạn, xoá hết phần tử sẽ phá vỡ giả định đó sớm.

TTL không miễn phí

200.000 khoá, giá trị 100 byte:

không TTL   178,1 byte/khoá
có TTL      212,6 byte/khoá      +34,4 byte  (+19%)

Mỗi TTL là một mục trong bảng expires: con trỏ tới khoá, dấu thời gian 8 byte, và phần cấu trúc của bảng băm.

19% không lớn, nhưng nó đáng biết khi bạn đặt TTL cho mọi khoá theo thói quen. Với dữ liệu thực sự vĩnh viễn, bỏ TTL tiết kiệm gần một phần năm.

Ngược lại — và quan trọng hơn — khoá không có TTL không bao giờ tự biến mất. Kiểm tra nhanh: INFO keyspace cho biết trong keys có bao nhiêu cái nằm trong expires.

db0:keys=200000,expires=200000,avg_ttl=3599662

expires nhỏ hơn keys nhiều nghĩa là phần lớn dữ liệu của bạn sẽ ở lại vĩnh viễn. Đó có thể đúng ý, hoặc là chỗ rò rỉ đang chờ.

Bốn lỗi thường gặp

Đặt TTL bằng lệnh riêng. SET k v rồi EXPIRE k 3600 là hai lệnh; giữa chúng tiến trình có thể chết và khoá ở lại mãi. Dùng SET k v EX 3600 — một lệnh, nguyên tử.

TTL đồng loạt. Một triệu khoá hết hạn cùng giây tạo ra một đợt xoá lớn và một đợt tải dồn về nguồn dữ liệu phía sau. Rải ra: EX 3600 + random(0, 300).

Dựa vào TTL để bảo đảm tính đúng đắn. Khoá phân tán dựa trên TTL sẽ hết hạn giữa lúc công việc còn chạy nếu công việc lâu hơn dự tính. Phần sau của sê-ri đo đúng chuyện này.

Quên KEEPTTL. Như trên — đây là lỗi hay gặp nhất trong danh sách.

Thử ba mươi giây

Xem bao nhiêu phần dữ liệu của bạn sẽ không bao giờ tự dọn:

redis-cli info keyspace

Rồi lấy mẫu để biết những khoá không TTL trông như thế nào:

redis-cli --scan --count 500 | head -3000 | while read k; do
  [ "$(redis-cli ttl "$k")" = "-1" ] && echo "$k"
done | sed 's/[0-9][0-9]*/N/g' | sort | uniq -c | sort -rn | head -15

Lệnh sed gộp các khoá cùng mẫu lại (user:12345user:67890 thành user:N), nên bảng in ra là các loại khoá không có hạn, kèm số lượng. Loại nào đứng đầu mà bạn không cố ý giữ vĩnh viễn thì đó là chỗ Redis đang lớn dần.

Phần sau: chính sách đuổi khoá — đo chuyện gì xảy ra khi Redis chạm maxmemory.