Redis sống trong RAM — mà RAM thì hữu hạn. Hai câu hỏi sống còn: làm sao để dữ liệu tạm (cache, session) tự dọn khi hết giá trị, và chuyện gì xảy ra khi bộ nhớ đầy? Redis trả lời bằng hai cơ chế: TTL (time-to-live — key tự hết hạn) và eviction (chính sách đẩy key ra khi chạm trần maxmemory). Chọn đúng cả hai là khác biệt giữa một Redis khoẻ mạnh tự dọn và một Redis hoặc phình RAM tới sập, hoặc từ chối ghi đúng lúc tải cao. Và điều nhiều người không ngờ: chính sách eviction mặc định có thể khiến ứng dụng lỗi khi Redis đầy. Bài này (phần 5/12) đo thật cả hai trong redis-lab.

TTL: cho key một hạn sống

Hầu hết dữ liệu trong Redis là tạm: cache nên hết hạn để không phục vụ dữ liệu cũ, session nên tự xoá sau một thời gian. TTL làm điều đó:

redis-cli SET sess:1 data EX 100   # sống 100 giây
redis-cli TTL sess:1               # đếm ngược giây còn lại
redis-cli EXPIRE k 60              # đặt hạn cho key đã có
redis-cli PERSIST k                # xoá hạn -> key sống mãi (TTL = -1)

Cơ chế xoá key hết hạn gồm hai phần: lazy (khi bạn truy cập một key đã hết hạn, Redis xoá nó ngay và trả nil) và active (một tác vụ nền quét định kỳ các key có hạn để xoá dần, kể cả khi không ai truy cập). Nhờ hai cơ chế này, bạn không cần tự dọn key cũ.

Ảnh chụp đoạn mã nền tối minh hoạ TTL và eviction key tự hết hạn và chuyện gì xảy ra khi bộ nhớ đầy, TTL cho key sống có thời hạn cache session eviction quyết định đẩy key nào ra khi RAM chạm trần. TTL đặt hạn sống cho key SET sess:1 data EX 100 sống 100 giây TTL sess:1 đếm ngược giây còn lại EXPIRE k 60 PEXPIRE k 500 đặt hạn giây mili-giây PERSIST k xoá hạn key sống mãi TTL bằng -1 xoá key hết hạn lazy khi truy cập cộng active quét định kỳ nền. Eviction khi used_memory vượt maxmemory chính sách maxmemory-policy noeviction mặc định từ chối lệnh ghi trả lỗi OOM allkeys-lru đẩy key ít dùng gần đây nhất mọi key allkeys-lfu đẩy key ít dùng thường xuyên nhất volatile-lru ttl chỉ đẩy key có đặt hạn allkeys-random đẩy ngẫu nhiên. Cấu hình giới hạn CONFIG SET maxmemory 3mb CONFIG SET maxmemory-policy allkeys-lru production đặt maxmemory khoảng RAM dành cho Redis chọn policy hợp use case

Hình 1: TTL đặt hạn sống (EX/PX/EXPIRE/PERSIST), key hết hạn xoá kiểu lazy + active. Eviction kích hoạt khi used_memory vượt maxmemory — bảng các chính sách: noeviction (từ chối ghi), allkeys-lru/lfu (đẩy key ít dùng), volatile- (chỉ đẩy key có hạn).*

Eviction: khi RAM chạm trần

Khi used_memory vượt maxmemory, Redis áp maxmemory-policy. Vài chính sách chính: noeviction (mặc định — từ chối lệnh ghi, trả lỗi OOM), allkeys-lru (đẩy key ít-dùng-gần-đây nhất), allkeys-lfu (ít-dùng-thường-xuyên nhất), volatile-lru/ttl (chỉ đẩy key có đặt hạn), allkeys-random.

Đo thật: TTL hết hạn và eviction

Ảnh chụp bảng kết quả đo thật TTL hết hạn và eviction khi đầy output thật redis:7 maxmemory 3mb nạp 100.000 key khoảng 100 byte. Một TTL key tự biến mất khi hết hạn SET sess:1 EX 100 TTL bằng 100 giây PERSIST sess:1 TTL bằng -1 không còn hạn SET tmp PX 150ms GET ngay bằng bay gio chờ 300ms GET bằng nil EXISTS bằng 0 key tự xoá key hết hạn bị xoá kiểu lazy khi truy cập cộng active quét nền định kỳ bạn không cần tự dọn. Hai allkeys-lru đẩy key cũ ghi vẫn thành công nạp 100.000 key vào giới hạn 3mb DBSIZE còn lại bằng 10.008 key phần còn lại bị đẩy evicted_keys bằng 89.992 số key bị đẩy ra used_memory bằng 3.00M giữ đúng trần không vượt Redis tự đẩy key ít dùng gần đây để nhường chỗ dùng như cache tự dọn bộ nhớ không bao giờ vượt trần. Ba noeviction ghi bị từ chối khi đầy cùng nạp 100.000 key policy bằng noeviction OOM command not allowed when used memory lớn hơn maxmemory DBSIZE dừng ở khoảng 10.126 key phần sau bị từ chối allkeys-lru Redis như cache đẩy cũ nhận mới noeviction Redis như store thà từ chối ghi còn hơn mất dữ liệu cũ chọn policy theo vai trò cache lru lfu nguồn dữ liệu noeviction cộng giám sát bộ nhớ

Hình 2: Đo thật. (1) TTL: SET EX 100 → TTL=100; PERSIST → TTL=-1; SET tmp PX 150ms, sau 300ms GET=nil, EXISTS=0. (2) allkeys-lru: nạp 100k key vào 3mb → DBSIZE 10.008, evicted_keys 89.992, used_memory 3.00M (ghi thành công). (3) noeviction: lỗi OOM, ghi bị từ chối.

Kết quả thật:

  • ① TTL tự dọn: SET sess:1 EX 100 → TTL=100; PERSIST → TTL=-1 (hết hạn bị xoá). SET tmp PX 150ms rồi GET ngay thấy "bay gio", nhưng sau 300ms GET tmp trả nil và EXISTS=0 — key tự biến mất. Không cần cron dọn dẹp.
  • ② allkeys-lru — cache tự dọn: đặt maxmemory 3mb, nạp 100.000 key (~100 byte/key, tổng ~10MB). Kết quả: DBSIZE còn 10.008 key, evicted_keys = 89.992 (số key bị đẩy ra), used_memory = 3.00M — giữ đúng trần, không vượt. Và quan trọng: mọi lệnh ghi đều thành công — Redis âm thầm đẩy key ít dùng để nhường chỗ. Đây là Redis hoạt động như một cache tự dọn.
  • ③ noeviction — ghi bị từ chối: cùng tải, policy mặc định noeviction. Khi đầy, Redis trả lỗi OOM command not allowed when used memory > 'maxmemory' và DBSIZE dừng ở ~10.126 key — phần sau bị từ chối ghi.

Khác biệt là bản chất: allkeys-lru coi Redis như cache (đẩy cũ, nhận mới — không bao giờ từ chối); noeviction coi Redis như store (thà từ chối ghi còn hơn làm mất dữ liệu cũ). Chọn sai là tai hoạ: một cache dùng noeviction sẽ ngừng nhận dữ liệu mới khi đầy (ứng dụng lỗi), còn một store quan trọng dùng allkeys-lru sẽ âm thầm vứt dữ liệu bạn tưởng còn.

Đánh đổi cần cân nhắc

Chọn chính sách theo VAI TRÒ của Redis. Redis làm cache thuần (dữ liệu có thể tái tạo từ nguồn) → allkeys-lru hoặc allkeys-lfu (lfu tốt hơn khi có key "nóng" được truy cập rất nhiều). Redis chứa dữ liệu không được mất (hàng đợi, lock) → noeviction + giám sát bộ nhớ để cảnh báo trước khi đầy. Redis lẫn lộn (cả cache lẫn dữ liệu bền) → dùng volatile-lru (chỉ đẩy key có đặt hạn, giữ key không hạn) và nhớ đặt TTL cho phần cache.

LRU của Redis là xấp xỉ, không chính xác. Để tiết kiệm bộ nhớ và CPU, Redis không theo dõi thứ tự truy cập của mọi key một cách chính xác — nó lấy mẫu ngẫu nhiên vài key (maxmemory-samples, mặc định 5) và đẩy cái cũ nhất trong mẫu. Nên key bị đẩy gần như là ít-dùng-nhất, không đảm bảo đúng tuyệt đối. Tăng maxmemory-samples cho chính xác hơn (tốn CPU hơn). Với hầu hết use case, xấp xỉ là đủ tốt.

TTL tốn bộ nhớ và active-expire có chi phí. Mỗi key có TTL lưu thêm thông tin hạn, và tác vụ active-expiration quét định kỳ — nếu rất nhiều key hết hạn cùng lúc, đợt quét có thể gây một nhịp CPU/latency tăng. Với khối lượng lớn, rải TTL (thêm jitter ngẫu nhiên vào thời hạn) để tránh "cơn bão hết hạn" đồng loạt. Và nhớ: key không có TTL sẽ không bao giờ tự xoá — dễ rò rỉ bộ nhớ nếu quên đặt hạn cho dữ liệu tạm.

Ba ý mang về

  1. TTL cho key tự hết hạn, không cần tự dọn. Đo thật: SET PX 150ms rồi sau 300ms GET trả nil, EXISTS=0. Redis xoá key hết hạn kiểu lazy (khi truy cập) + active (quét nền). Luôn đặt TTL cho dữ liệu tạm (cache/session).
  2. Eviction policy quyết định hành vi khi đầy — chọn theo vai trò. Đo thật với maxmemory 3mb: allkeys-lru đẩy 89.992 key và ghi vẫn thành công (cache tự dọn, memory giữ 3.00M); noeviction từ chối ghi với lỗi OOM (store, không mất dữ liệu cũ).
  3. Mỗi cơ chế có cái giá. LRU của Redis là xấp xỉ (lấy mẫu, không chính xác tuyệt đối); active-expire có thể gây nhịp CPU khi nhiều key hết hạn cùng lúc (rải TTL bằng jitter); và key không TTL không bao giờ tự xoá — dễ rò rỉ nếu quên.

Nguồn

Phần sau ta sang nhắn tin: Pub/Sub cho realtime fire-and-forget và Streams cho log sự kiện bền — hai cơ chế dễ nhầm, khác nhau căn bản ở chỗ giữ hay không giữ message.