📌 Giới Thiệu Về Giới Hạn Tốc Độ Fixed Window
Giới Hạn Tốc Độ Fixed Window là một thuật toán đơn giản kiểm soát tốc độ yêu cầu bằng cách chia thời gian thành các khoảng thời gian cố định (các cửa sổ) và cho phép một số lượng yêu cầu tối đa trong mỗi cửa sổ.
Ví dụ:
Nếu một API cho phép 100 yêu cầu mỗi phút:
- Bộ đếm sẽ được đặt lại vào đầu mỗi phút.
- Người dùng thực hiện 100 yêu cầu vào 00:59:59 có thể ngay lập tức thực hiện thêm 100 yêu cầu nữa sau 01:00:00. Điều này có thể gây ra sự tăng đột biến vào các ranh giới của cửa sổ.
📝 Tình Huống Ví Dụ
-
Trường Hợp Sử Dụng: API đăng nhập với giới hạn 10 lần thử mỗi phút.
-
Hành Vi:
- Người dùng có thể thử 10 lần trong phút hiện tại.
- Sau khi cửa sổ được đặt lại, bộ đếm sẽ làm mới, cho phép thêm 10 lần thử nữa.
✅ Lợi Ích
- Đơn giản và dễ triển khai.
- Tổn thất tài nguyên và chi phí tối thiểu.
- Dễ dàng gỡ lỗi và hiểu.
⚠️ Hạn Chế
- Sự Tăng Đột Biến: Cho phép các đợt tăng đột biến tại các ranh giới cửa sổ.
- Độ Chính Xác: Ít mượt mà hơn so với các phương pháp cửa sổ trượt.
- Thách Thức Phân Tán: Bộ đếm trong bộ nhớ đơn sẽ không hoạt động trên nhiều phiên bản mà không có sự phối hợp trung tâm.
💻 Triển Khai Trên Một Máy (Java An Toàn Luồng)
Khi Nào Sử Dụng:
- Dịch vụ quy mô nhỏ.
- Các điểm cuối không quan trọng.
- Dịch vụ nội bộ nơi không cần sự phối hợp phân tán.
Mã cho Giới Hạn Tốc Độ Fixed Window Trên Một Máy:
java
import java.time.Duration;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
/**
* Giới hạn tốc độ Fixed Window trên một máy.
* Triển khai an toàn với luồng cho các dịch vụ quy mô nhỏ.
*/
public class FixedRateLimiter implements IRateLimiter {
private final Timer timer; // Cung cấp thời gian hiện tại (có thể tiêm cho kiểm tra)
private final Map<String, FixedWindow> map; // Lưu trữ số lượng yêu cầu theo người dùng/requestId
private final Duration windowSize; // Kích thước của cửa sổ thời gian cố định
private final int capacity; // Số yêu cầu tối đa trong mỗi cửa sổ
public FixedRateLimiter(Duration windowSize, int capacity, Timer timer) {
this.timer = timer;
this.map = new ConcurrentHashMap<>();
this.windowSize = windowSize;
this.capacity = capacity;
}
/**
* Kiểm tra xem yêu cầu có được phép cho requestId đã cho hay không.
*
* @param requestId Định danh duy nhất cho client/người dùng
* @return true nếu được phép, false nếu vượt quá giới hạn tốc độ
*/
@Override
public boolean isAllowed(String requestId) {
long currentTimeMillis = timer.currentTimeMillis();
// Lấy hoặc tạo FixedWindow cho requestId này
FixedWindow fixedWindow = map.computeIfAbsent(
requestId,
e -> new FixedWindow(new AtomicInteger(capacity), currentTimeMillis)
);
// Đặt lại cửa sổ nếu thời gian hiện tại vượt quá cửa sổ trước đó
if (currentTimeMillis - fixedWindow.lastAccessTime > windowSize.toMillis()) {
synchronized (fixedWindow) {
if (currentTimeMillis - fixedWindow.lastAccessTime > windowSize.toMillis()) {
fixedWindow.lastAccessTime = currentTimeMillis;
fixedWindow.requestCount.set(capacity); // Đặt lại số lượng yêu cầu
}
}
}
// Cho phép yêu cầu nếu còn dung lượng
int remaining = fixedWindow.requestCount.get();
if (remaining > 0) {
fixedWindow.requestCount.decrementAndGet();
return true;
}
return false; // Từ chối nếu đã đạt giới hạn
}
/**
* Lớp nội bộ để lưu trữ số lượng yêu cầu và thời gian truy cập cuối cùng cho mỗi cửa sổ.
*/
private static class FixedWindow {
final AtomicInteger requestCount; // Theo dõi số yêu cầu còn lại
volatile long lastAccessTime; // Thời gian bắt đầu của cửa sổ
FixedWindow(AtomicInteger requestCount, long lastAccessTime) {
this.requestCount = requestCount;
this.lastAccessTime = lastAccessTime;
}
}
/**
* Trừu tượng Timer cho dễ kiểm tra (có thể tiêm nhà cung cấp thời gian hiện tại)
*/
public record Timer() {
public long currentTimeMillis() {
return System.currentTimeMillis();
}
}
}
🌐 Triển Khai Phân Tán (Redis + Java)
Khi Nào Sử Dụng:
- APIs quy mô lớn trên nhiều phiên bản.
- Yêu cầu phối hợp trung tâm để ngăn người dùng vượt quá giới hạn toàn cầu.
Mã cho Giới Hạn Tốc Độ Fixed Window Dựa Trên Redis:
java
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
/**
* Giới hạn tốc độ Fixed Window phân tán sử dụng Redis.
* Phù hợp cho các ứng dụng đa phiên bản.
*/
public class RedisFixedRateLimiter {
private final JedisPool jedisPool; // Pool kết nối Redis
private final String rateLimitKeyPrefix; // Tiền tố cho các khóa Redis, ví dụ: "rate_limit:"
private final int maxRequestsPerWindow; // Số yêu cầu tối đa cho phép trong mỗi cửa sổ
private final int windowDurationInSeconds; // Kích thước cửa sổ tính bằng giây
public RedisFixedRateLimiter(JedisPool jedisPool,
String rateLimitKeyPrefix,
int maxRequestsPerWindow,
int windowDurationInSeconds) {
this.jedisPool = jedisPool;
this.rateLimitKeyPrefix = rateLimitKeyPrefix;
this.maxRequestsPerWindow = maxRequestsPerWindow;
this.windowDurationInSeconds = windowDurationInSeconds;
}
/**
* Kiểm tra xem yêu cầu có được phép cho userId đã cho hay không.
*
* @param userId Định danh duy nhất cho client/người dùng
* @return true nếu yêu cầu được phép, false nếu vượt quá giới hạn tốc độ
*/
public boolean isRequestAllowed(String userId) {
String key = rateLimitKeyPrefix + userId;
try (Jedis jedis = jedisPool.getResource()) {
// Tăng số lượng yêu cầu trong Redis một cách nguyên tử
long currentCount = jedis.incr(key);
// Đặt thời gian hết hạn chỉ cho yêu cầu đầu tiên trong cửa sổ
if (currentCount == 1) {
jedis.expire(key, windowDurationInSeconds);
}
// Cho phép nếu số lượng không vượt quá tối đa
return currentCount <= maxRequestsPerWindow;
} catch (Exception e) {
// Chính sách fail-open: cho phép yêu cầu nếu Redis không khả dụng
e.printStackTrace();
return true;
}
}
}
⚡ Độ Tin Cậy Cho Redis
- Redis Down: Triển khai chính sách fail-open (cho phép yêu cầu) hoặc bộ đếm dự phòng cục bộ.
- Circuit Breaker: Tạm thời dừng lưu lượng truy cập quá mức khi Redis không thể truy cập.
- Giảm Thiểu Mức Độ: Sử dụng bộ đếm trong bộ nhớ với TTL ngắn để giới hạn tác động cho đến khi Redis phục hồi.
✅ Tóm Tắt
- Giới Hạn Tốc Độ Fixed Window là đơn giản, hiệu quả và hiệu quả cho nhiều trường hợp sử dụng đơn giản.
- Hạn chế chính: sự tăng đột biến tại các ranh giới cửa sổ.
- Cách tiếp cận trên một máy: Phù hợp cho các dịch vụ ít lưu lượng, không quan trọng.
- Cách tiếp cận dựa trên Redis: Lý tưởng cho môi trường phân tán có lưu lượng lớn nhưng cần lập kế hoạch độ tin cậy.