Nguyên nhân: mỗi instance giữ bộ đếm riêng trong RAM, nên hạn mức bị nhân lên theo số instance — và con số này còn đổi khi autoscaling. Bộ đếm phải nằm ở kho dùng chung, thường là Redis.
Token bucket trên Redis cần đọc trạng thái, tính lại số token theo thời gian trôi qua rồi ghi lại. Ba bước này phải nguyên tử, nếu không hai request đồng thời cùng thấy còn token và cùng cho qua. Cách chuẩn là gói vào một script Lua — Redis chạy script đơn luồng nên toàn bộ script là một thao tác nguyên tử:
-- KEYS[1] = bucket key, ARGV = rate, capacity, now, cost
local state = redis.call('HMGET', KEYS[1], 'tokens', 'ts')
local tokens = tonumber(state[1]) or tonumber(ARGV[2])
local ts = tonumber(state[2]) or tonumber(ARGV[3])
tokens = math.min(tonumber(ARGV[2]), tokens + (ARGV[3] - ts) * ARGV[1])
if tokens < tonumber(ARGV[4]) then return 0 end
redis.call('HMSET', KEYS[1], 'tokens', tokens - ARGV[4], 'ts', ARGV[3])
return 1Những điểm người phỏng vấn hay đào thêm:
- Thêm một vòng mạng cho mỗi request. Giảm bằng cách cấp phát token theo lô: instance xin trước 10 token rồi tiêu dần cục bộ, đổi độ chính xác lấy độ trễ.
- Redis chết thì sao? Phải quyết định trước: fail-open (cho qua, ưu tiên khả dụng) hay fail-closed (chặn, ưu tiên bảo vệ). Đa số API công khai chọn fail-open kèm hạn mức dự phòng cục bộ.
- Trả về đúng chuẩn: HTTP 429 kèm Retry-After để client biết chờ bao lâu, tránh client retry dồn dập.
- Rate limit chỉ chặn lạm dụng; quá tải do lưu lượng hợp lệ vẫn cần load shedding ở tầng ngoài.