"Mỗi user chỉ được gọi API này 100 lần một phút." "Chỉ cho gửi 5 OTP một giờ." Rate limiting — giới hạn số request trong một khoảng thời gian — có mặt ở gần như mọi hệ thống backend để chống lạm dụng, chống brute-force (nối với sê-ri bảo mật), và bảo vệ tài nguyên. Và Redis là công cụ lý tưởng cho việc này: nhanh, có bộ đếm nguyên tử (INCR), TTL, và sorted set. Nhưng có nhiều thuật toán rate limiting, mỗi cái đánh đổi khác nhau giữa đơn giản, chính xác, và bộ nhớ. Bài này (phần 9/12) đo thật ba thuật toán phổ biến nhất trong redis-lab.
Ba thuật toán

Hình 1: Ba thuật toán rate limiting. Fixed window (INCR+EXPIRE, đơn giản nhưng burst ở ranh giới cửa sổ); sliding window (sorted set theo timestamp, mượt, không burst); token bucket (refill token theo thời gian, cho phép burst có kiểm soát).
- Fixed window:
INCRmột key mỗi request,EXPIREcho key hết hạn sau mỗi cửa sổ (ví dụ 60s). Nếu count ≤ limit thì cho qua, vượt thì chặn (429). Đơn giản nhất — một lệnh. Nhược điểm: burst ở ranh giới — 5 request cuối cửa sổ A + 5 request đầu cửa sổ B = 10 request trong ~1 giây, gấp đôi limit. - Sliding window: dùng sorted set, mỗi request là một phần tử với score = timestamp. Mỗi lần:
ZREMRANGEBYSCORExoá request cũ (ngoài cửa sổ),ZCARDđếm request trong cửa sổ trượt, nếu < limit thìZADDthêm request này. Cửa sổ trượt theo thời gian thực → không burst ở ranh giới. - Token bucket: bucket chứa tối đa N token, refill r token/giây; mỗi request tiêu 1 token, hết token thì chặn. Cho phép burst có kiểm soát (dồn request dùng cả bucket rồi chờ refill) — mượt cho traffic thật.
Đo thật: fixed window và sliding window
Với limit = 5, mình gửi 7 request liên tiếp:

Hình 2: Đo thật với limit 5. (1) Fixed window: request 1-5 cho qua (count ≤ 5), request 6-7 bị chặn (429). (2) Sliding window: trả [cho-qua, count] — request 1-5 [1,1..5] cho qua, request 6-7 [0,5] bị chặn, count giữ ở 5.
Kết quả thật:
- ① Fixed window: request 1-5 cho qua (count 1→5 ≤ 5), request 6-7 bị chặn (count 6, 7 > 5). Chỉ một lệnh
INCR(cộngEXPIRElần đầu) — cực nhẹ. Nhưng nhớ nhược điểm ranh giới: nếu 5 request này ở cuối cửa sổ và 5 request nữa đến đầu cửa sổ sau, server nhận 10 request trong một khoảng ngắn. - ② Sliding window: trả về
[cho-qua, count]. Request 1-5 trả[1, 1..5](cho qua), request 6-7 trả[0, 5](chặn, count giữ ở 5). Đếm theo cửa sổ trượt 10 giây gần nhất —ZREMRANGEBYSCORExoá các request cũ ngoài cửa sổ,ZCARDđếm. Không có burst gấp đôi như fixed window, vì cửa sổ luôn tính "10 giây vừa qua tính từ bây giờ", không phải "cửa sổ cố định reset lúc chẵn phút".
Đổi lại: sliding window tốn bộ nhớ hơn (lưu một entry timestamp cho mỗi request trong cửa sổ) và phức tạp hơn (cần Lua để xoá-đếm-thêm nguyên tử, tránh race — bài redis-04). Fixed window đơn giản và rẻ nhưng kém chính xác ở ranh giới.
Đánh đổi cần cân nhắc
Chọn thuật toán theo mức chính xác CẦN. Fixed window đủ cho hầu hết trường hợp "chống lạm dụng thô" (giới hạn gọi API chung) — burst gấp đôi ở ranh giới thường không nghiêm trọng, và nó rẻ nhất. Sliding window đáng dùng khi chính xác quanh ranh giới quan trọng (ví dụ "đúng 5 OTP/giờ, không được 10"). Token bucket hợp khi muốn cho phép burst nhưng giới hạn tốc độ trung bình (API cho dồn vài request rồi đều lại). Đừng dùng sliding window phức tạp khi fixed window đã đủ.
Rate limit phải nguyên tử — đừng GET-rồi-SET. Cạm bẫy phổ biến: đọc count, kiểm, rồi ghi count+1 trong ba lệnh riêng — hai request đồng thời cùng đọc count=4, cùng thấy "còn chỗ", cùng cho qua → vượt limit. Phải dùng thao tác nguyên tử: INCR (fixed window tự nguyên tử) hoặc một script Lua (sliding window). Đây chính là lý do sliding window cần Lua — nối thẳng bài redis-04.
Rate limit phân tán và đồng hồ. Nếu nhiều instance app cùng giới hạn một user, chúng phải dùng chung một Redis (không phải mỗi instance một bộ đếm riêng — sẽ thành limit × số instance). Và sliding window dùng timestamp — nếu dùng thời gian của client (nhiều máy lệch đồng hồ) sẽ sai; tốt hơn dùng TIME của Redis (đồng hồ một nguồn) hoặc truyền timestamp nhất quán. Chi tiết nhỏ nhưng gây lỗi khó lần.
Ba ý mang về
- Redis là công cụ lý tưởng cho rate limiting nhờ thao tác nguyên tử. Đo thật với limit 5: fixed window (INCR+EXPIRE) cho 5 request qua, chặn 6-7; sliding window (sorted set+Lua) cũng vậy nhưng đếm theo cửa sổ trượt.
- Mỗi thuật toán đánh đổi đơn giản vs chính xác vs bộ nhớ. Fixed window rẻ nhất (1 lệnh) nhưng burst ở ranh giới; sliding window mượt/chính xác hơn nhưng tốn bộ nhớ (lưu timestamp mỗi request) và cần Lua; token bucket cho phép burst có kiểm soát.
- Rate limit phải nguyên tử và dùng chung một Redis. Đừng GET-rồi-SET (race làm vượt limit) — dùng INCR hoặc Lua; nhiều instance phải chia sẻ một bộ đếm Redis; và sliding window nên dùng đồng hồ một nguồn để tránh lệch thời gian.
Nguồn
- Redis — Rate limiting patterns: https://redis.io/docs/latest/develop/use/patterns/
- Redis — INCR for rate limiting: https://redis.io/commands/incr/
- Cloudflare — How we built rate limiting (sliding window): https://blog.cloudflare.com/counting-things-a-lot-of-different-things/
Phần sau ta tìm hiểu Redis có thật sự mất dữ liệu khi chết không: hai cơ chế persistence RDB và AOF, chúng ghi xuống đĩa thế nào, và đánh đổi giữa độ bền và hiệu năng.