+3

Series Redis Thực chiến | Bài 3: Rate Limiting - "Lá chắn thép" chống bão Spam API và DDoS

Chào anh em! Khi API của anh em được public ra ngoài, sẽ luôn có những vị khách "không mời mà đến": Bot cào dữ liệu (Crawler), tool spam click, hoặc tệ hơn là những đợt tấn công từ chối dịch vụ (DDoS) cơ bản.

Nếu anh em để mặc cho mọi request chọc thẳng vào Application (PHP/Node.js) rồi truy vấn xuống Database, server sẽ kiệt sức và "ngỏm" chỉ trong vài phút.

Bài toán đặt ra là: Làm sao để chặn đứng các request vượt ngưỡng cho phép (ví dụ: 1 IP chỉ được gọi API 60 lần/phút) với tốc độ nhanh nhất có thể? Database không thể làm việc này vì quá chậm. Câu trả lời hoàn hảo nhất chính là Redis Rate Limiting.

1. Thuật toán "ngây thơ": Fixed Window Counter (Cửa sổ cố định)

Đây là cách dễ hiểu nhất và phổ biến nhất để làm Rate Limiting. Chúng ta sẽ dùng 2 lệnh cơ bản của Redis là INCR (tăng biến đếm lên 1) và EXPIRE (set thời gian hủy).

Cách hoạt động: Giả sử bạn muốn giới hạn IP 192.168.1.5 chỉ được gọi API Đăng nhập 5 lần trong 1 phút.

  1. Khi có request, tạo một Key trong Redis: rate_limit:login:192.168.1.5:2026-06-30-23-15 (Key gắn với số phút hiện tại).
  2. Tăng giá trị Key này lên bằng lệnh INCR.
  3. Set thời gian tồn tại cho Key là 60 giây bằng lệnh EXPIRE.
  4. Nếu giá trị trả về > 5, lập tức chặn Request và ném lỗi 429 Too Many Requests.

Đoạn code minh họa (PHP thuần túy thao tác Redis):

$ip = $request->ip();
$currentMinute = date('Y-m-d-H-i');
$redisKey = "rate_limit:login:{$ip}:{$currentMinute}";

// Tăng biến đếm
$requestsCount = Redis::incr($redisKey);

// Nếu là request đầu tiên trong phút này, set thời gian hết hạn là 60s
if ($requestsCount == 1) {
    Redis::expire($redisKey, 60);
}

// Kiểm tra giới hạn
if ($requestsCount > 5) {
    return response()->json(['error' => 'Bạn thao tác quá nhanh, vui lòng thử lại sau!'], 429);
}

// Cho phép đi tiếp vào logic xử lý...

Nhược điểm chí mạng (Vấn đề "Thủng mốc thời gian"): Thuật toán này có một kẽ hở lớn. Nếu một hacker gửi 5 request ở giây thứ 59 của phút 14, và gửi tiếp 5 request ở giây thứ 1 của phút 15. Hệ thống vẫn cho qua cả 10 request này! Thực tế, server đã phải gánh 10 request chỉ trong vòng 2 giây (vượt xa giới hạn 5 req/phút ban đầu).

2. Giải pháp hoàn hảo: Thuật toán Sliding Window (Cửa sổ trượt)

Để giải quyết bài toán "Thủng mốc thời gian", các hệ thống lớn dùng một cấu trúc dữ liệu xịn sò hơn của Redis: Sorted Set (ZSET).

Ý tưởng là: Chúng ta không đếm theo từng mốc phút cố định nữa. Khi có một request mới, ta nhìn ngược lại chính xác 60 giây về trước (từ thời điểm hiện tại now() đến now() - 60s) xem có bao nhiêu request đã được ghi nhận.

Cách này chính xác 100% từng mili-giây, không thể bị qua mặt!

3. Thực chiến Rate Limiting cực "nhàn" với Laravel

Lý thuyết để anh em hiểu bản chất, còn khi vào trận mạc thực tế, các framework hiện đại đã bọc sẵn các đoạn mã Lua Script phức tạp của Redis lại cho chúng ta.

Nếu anh em dùng Laravel, chức năng Rate Limiting được tích hợp thẳng vào Route Middleware thông qua facade RateLimiter. Mặc định Laravel sẽ dùng thuật toán tối ưu trên Redis (nếu anh em set Cache Driver là Redis).

Bước 1: Định nghĩa bộ giới hạn (Trong app/Providers/RouteServiceProvider.php)

use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Support\Facades\RateLimiter;

public function boot()
{
    // Cấu hình: API Login chỉ cho phép 5 request / 1 phút / 1 IP
    RateLimiter::for('login_limit', function (Request $request) {
        return Limit::perMinute(5)->by($request->ip());
    });
    
    // Cấu hình: API Thanh toán nội bộ chỉ cho gọi 100 request / phút / 1 User ID
    RateLimiter::for('payment_limit', function (Request $request) {
        return Limit::perMinute(100)->by($request->user()->id);
    });
}

Bước 2: Gắn vào Route

// Gắn lá chắn 'login_limit' vào route
Route::post('/login', [AuthController::class, 'login'])->middleware('throttle:login_limit');

// Gắn lá chắn 'payment_limit' vào route thanh toán
Route::post('/checkout', [OrderController::class, 'checkout'])->middleware('throttle:payment_limit');

Bây giờ, chỉ cần ai đó vượt ngưỡng, Laravel và Redis sẽ bắt tay nhau tự động chặn đứng ở ngay cửa ngõ bằng HTTP Status Code 429, bảo vệ tuyệt đối an toàn cho các dòng code nghiệp vụ và Database bên trong.

Tổng kết Bài 3

Bằng việc đưa Rate Limiting lên thẳng tầng RAM (Redis), anh em đã tạo ra một "lá chắn thép" cản phá hầu hết các nỗ lực spam và DDoS ở mức độ cơ bản. Kiến thức này đặc biệt hữu dụng khi anh em thiết kế API Public hoặc các cổng thanh toán.

Đã có "Khóa phân tán" và "Lá chắn", ở Bài 4 tiếp theo, bạn muốn chúng ta tìm hiểu về tính năng Pub/Sub (Giao tiếp thời gian thực, Notification) hay chuyển sang khám phá Redis Streams (hàng đợi Message Broker siêu tốc)?


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí