Đặt TTL là thao tác phổ biến nhất trong Redis sau GET và SET. 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.
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ế; APPEND và INCRBY 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:12345 và user: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.