Gọi API quá nhiều lần trong một phút, bạn nhận lỗi 429 Too Many Requests — đó là rate limiter đang hoạt động. Tìm hiểu 3 thuật toán phổ biến và cách xây dựng chúng với Redis trong vài dòng lệnh.
10 phút đọc•
Mục lục bài viết23
Bạn có biết?
Khi bạn gọi API của Shopee hay Facebook quá nhiều lần trong 1 phút, hệ thống sẽ trả về lỗi 429 Too Many Requests. Đó chính là Rate Limiter đang hoạt động. Với Redis, bạn có thể xây dựng rate limiter chỉ với vài dòng lệnh — nhanh, chính xác, và xử lý hàng triệu request mỗi giây.
Rate Limiter là gì?
Rate Limiter giới hạn số lần một user hoặc IP có thể thực hiện hành động trong khoảng thời gian nhất định. Mục tiêu:
Bảo vệ API — Chống abuse, DDoS
Quản lý tài nguyên — Tránh quá tải server
Fair usage — Đảm bảo công bằng giữa các user
Giảm chi phí — Giới hạn call tới API trả phí (OpenAI, Stripe…)
Các thuật toán phổ biến
Bài này sẽ implement cả 3 cách phổ biến nhất với Redis.
Thuật toán
Cơ chế
Ưu điểm
Nhược điểm
Fixed Window
Đếm trong khung thời gian cố định
Đơn giản, dễ implement
Burst ở biên cửa sổ
Sliding Window Log
Lưu timestamp từng request
Chính xác nhất
Tốn bộ nhớ
Sliding Window Counter
Kết hợp Fixed Window + tỷ lệ
Cân bằng hiệu suất
Không hoàn toàn chính xác
Token Bucket
Thêm token theo thời gian
Cho phép burst
Phức tạp hơn
Cách 1: Fixed Window Counter
Đơn giản nhất — đếm số request trong khung thời gian cố định (ví dụ: 1 phút).
Cơ chế
Key: rate:user:{id}:{window}
Mỗi request → INCR counter
Nếu counter > limit → từ chối
Key tự hết hạn sau window size
Redis CLI
# User 123 gửi request đầu tiên trong phút hiện tại127.0.0.1:6379>INCRrate:user:123:1711785600
(integer) 1# Đặt TTL lần đầu (chỉ set 1 lần)
(integer)
(integer)
Ứng dụng chạy ngon ở dev nhưng lên production, Redis vừa restart là mọi request treo 30 giây rồi trả 500. Vấn đề nằm ở cách quản lý kết nối — bài này dạy bạn cấu hình ioredis để hệ thống sống khỏe.
Ứng dụng giao đồ ăn hiện 20 quán quanh mình, sắp xếp theo khoảng cách chỉ trong mili giây — không cần database spatial hay Google Maps API, chỉ với vài lệnh Geo xây trên nền Sorted Sets.
Đếm DAU cho 10 triệu user bằng Set tốn hàng trăm MB RAM, nhưng HyperLogLog chỉ cần 12KB và Bitmap theo dõi trạng thái từng user với vỏn vẹn 1.25MB. Hai vũ khí bí mật cho analytics quy mô lớn.
1 phút đọc
127.0.0.1:6379>
EXPIRE
rate:
user:
123
:
1711785600
60
1
# Request thứ 2, 3, 4...
127.0.0.1:6379>
INCR
rate:
user:
123
:
1711785600
2
# Kiểm tra số request hiện tại
127.0.0.1:6379>
GET
rate:
user:
123
:
1711785600
"2"
# Nếu vượt quá 100 request/phút → trả lỗi 429
Python Implementation
import redis
import time
r = redis.Redis(host='localhost', port=6379)
def is_allowed_fixed_window(user_id, limit=100, window=60):
current_window = int(time.time()) // window
key = f"rate:user:{user_id}:{current_window}"pipe = r.pipeline()
pipe.incr(key)
pipe.expire(key, window)
count, _ = pipe.execute()
return count <= limit
# Sử dụng
if is_allowed_fixed_window("user_123", limit=100, window=60):
print("Request được phép")
else:
print("429 Too Many Requests")
Vấn đề của Fixed Window
Giới hạn 100 request/phút. User gửi 100 request ở giây cuối của phút 1, rồi 100 request ở giây đầu của phút 2 → 200 request trong 2 giây! Đây gọi là burst ở biên cửa sổ.
Cách 2: Sliding Window Log
Lưu timestamp của từng request. Khi có request mới, loại bỏ các timestamp cũ hơn window.
import redis
import time
r = redis.Redis(host='localhost', port=6379)
def is_allowed_sliding_window(user_id, limit=100, window=60):
key = f"rate:log:{user_id}"now = time.time()
window_start = now - window
pipe = r.pipeline()
# Xóa request cũ
pipe.zremrangebyscore(key, 0, window_start)
# Thêm request hiện tại
pipe.zadd(key, {str(now): now})
# Đếm request trong window
pipe.zcard(key)
# Đặt TTL
pipe.expire(key, window)
_, _, count, _ = pipe.execute()
return count <= limit
Ưu điểm
Chính xác 100% — không có burst ở biên cửa sổ. Nhưng tốn bộ nhớ vì lưu từng timestamp.
Cách 3: Token Bucket
Mỗi user có một "bucket" chứa token. Token được thêm vào theo tốc độ cố định. Mỗi request tiêu tốn 1 token. Hết token → từ chối.
Cơ chế
Bucket có sức chứa tối đa (burst capacity)
Token được thêm theo rate (ví dụ: 10 token/giây)
Request lấy 1 token từ bucket
Bucket đầy → cho phép burst ngắn hạn
Redis CLI
# Lưu trạng thái bucket bằng Hash
127.0.0.1:6379> HSET rate:bucket:user:123 tokens 10 last_refill 1711785600
(integer) 2
# Khi có request → kiểm tra và cập nhật# (Thường dùng Lua script để đảm bảo atomic)# Lua script:
eval "
local key = KEYS[1]
local rate = tonumber(ARGV[1]) -- tokens per second
local capacity = tonumber(ARGV[2]) -- max tokens
local now = tonumber(ARGV[3]) -- current time
local bucket = redis.call('HMGET', key, 'tokens', 'last_refill')
local tokens = tonumber(bucket[1]) or capacity
local last_refill = tonumber(bucket[2]) or now
-- Tính token mới
local elapsed = now - last_refill
local new_tokens = math.min(capacity, tokens + elapsed * rate)
if new_tokens >= 1 then
redis.call('HMSET', key, 'tokens', new_tokens - 1, 'last_refill', now)
redis.call('EXPIRE', key, 60)
return 1 -- allowed
else
redis.call('HMSET', key, 'tokens', new_tokens, 'last_refill', now)
redis.call('EXPIRE', key, 60)
return 0 -- denied
end
" 1 rate:bucket:user:123 10 20 1711785610
Python Implementation
import redis
import time
r = redis.Redis(host='localhost', port=6379)
LUA_SCRIPT = """
local key = KEYS[1]
local rate = tonumber(ARGV[1])
local capacity = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local bucket = redis.call('HMGET', key, 'tokens', 'last_refill')
local tokens = tonumber(bucket[1]) or capacity
local last_refill = tonumber(bucket[2]) or now
local elapsed = now - last_refill
local new_tokens = math.min(capacity, tokens + elapsed * rate)
if new_tokens >= 1 then
redis.call('HMSET', key, 'tokens', new_tokens - 1, 'last_refill', now)
redis.call('EXPIRE', key, 60)
return 1
else
redis.call('HMSET', key, 'tokens', new_tokens, 'last_refill', now)
redis.call('EXPIRE', key, 60)
return 0
end
"""token_bucket = r.register_script(LUA_SCRIPT)
def is_allowed_token_bucket(user_id, rate=10, capacity=20):
key = f"rate:bucket:{user_id}"now = time.time()
result = token_bucket(keys=[key], args=[rate, capacity, now])
return result == 1# Sử dụng
for i in range(25):
allowed = is_allowed_token_bucket("user_123", rate=10, capacity=20)
print(f"Request {i+1}: {'✅ Allowed' if allowed else '❌ Denied'}")
So sánh 3 thuật toán
Tiêu chí
Fixed Window
Sliding Window Log
Token Bucket
Độ chính xác
Trung bình
Cao nhất
Cao
Bộ nhớ
Ít nhất
Nhiều nhất
Trung bình
Performance
Nhanh nhất
Chậm nhất
Nhanh
Cho phép burst
Không kiểm soát
Không
Có kiểm soát
Phù hợp
API đơn giản
Cần chính xác
Production
Use Cases thực tế
1. API Gateway
# Giới hạn 1000 request/giờ cho mỗi API keydefcheck_api_limit(api_key):
return is_allowed_token_bucket(
f"api:{api_key}",
rate=1000/3600, # ~0.28 token/giây
capacity=1000
)
2. Login Protection
# Giới hạn 5 lần đăng nhập sai trong 15 phútdefcheck_login_attempts(ip_address):
return is_allowed_fixed_window(
f"login:{ip_address}",
limit=5,
window=900# 15 phút
)
0 bình luận
Đang tải bình luận...
Để lại bình luận