Tối Ưu Hóa Truyền Tải Dữ Liệu Lớn với Laravel Streaming Responses
Nguồn gốc: Laravel News – Optimizing Large Data Delivery with Laravel Streaming Responses
Tác giả: Harris Raftopoulos
Ngày xuất bản: 7 tháng 1, 2025
Giới thiệu
Tính năng Streaming Response trong Laravel cho phép xử lý hiệu quả các tập dữ liệu lớn bằng cách gửi dữ liệu theo từng phần nhỏ ngay khi chúng được tạo ra, thay vì phải chờ toàn bộ dữ liệu sẵn sàng mới gửi đi. Phương pháp này giúp:
- Giảm mức sử dụng bộ nhớ (RAM) trên server
- Cải thiện thời gian phản hồi – người dùng nhận được dữ liệu ngay lập tức
- Tránh timeout khi xử lý tác vụ nặng, tốn thời gian
- Nâng cao trải nghiệm người dùng khi xuất file hoặc tải dữ liệu lớn
Cơ bản: Cách hoạt động của Streaming
Laravel cung cấp helper response()->stream() để trả về dữ liệu dạng luồng. Cấu trúc cơ bản như sau:
Route::get('/stream', function () {
return response()->stream(function () {
foreach (range(1, 100) as $number) {
echo "Line {$number}\n";
ob_flush(); // Xả bộ đệm output của PHP
flush(); // Gửi dữ liệu xuống client ngay lập tức
}
}, 200, ['Content-Type' => 'text/plain']);
});
Giải thích chi tiết:
| Thành phần | Mục đích |
|---|---|
response()->stream(callback, status, headers) |
Tạo một streaming response |
echo |
Ghi dữ liệu vào output buffer |
ob_flush() |
Xả output buffer của PHP |
flush() |
Buộc PHP gửi dữ liệu xuống web server và client |
Lưu ý quan trọng: Cần gọi cả
ob_flush()vàflush()để đảm bảo dữ liệu được gửi đi ngay lập tức, không bị giữ lại trong buffer.
Ví dụ thực tế: Xuất CSV dữ liệu đơn hàng lớn
Đây là trường hợp điển hình nhất của streaming – xuất hàng trăm nghìn bản ghi ra file CSV mà không làm quá tải bộ nhớ server.
<?php
namespace App\Http\Controllers;
use App\Models\Order;
use Illuminate\Support\Facades\DB;
class ExportController extends Controller
{
public function exportOrders()
{
return response()->stream(function () {
// Gửi header của file CSV
echo "Order ID,Customer,Total,Status,Date\n";
ob_flush();
flush();
// Xử lý đơn hàng theo từng đợt (chunk) để tiết kiệm bộ nhớ
Order::query()
->with('customer')
->orderBy('created_at', 'desc')
->chunk(500, function ($orders) {
foreach ($orders as $order) {
echo sprintf(
"%s,%s,%.2f,%s,%s\n",
$order->id,
str_replace(',', ' ', $order->customer->name), // Tránh lỗi CSV do dấu phẩy
$order->total,
$order->status,
$order->created_at->format('Y-m-d H:i:s')
);
ob_flush();
flush();
}
});
}, 200, [
'Content-Type' => 'text/csv',
'Content-Disposition' => 'attachment; filename="orders.csv"',
'X-Accel-Buffering' => 'no', // Tắt buffering của Nginx
]);
}
}
Phân tích các kỹ thuật được sử dụng:
1. Chunking – Phân trang dữ liệu từ database
->chunk(500, function ($orders) { ... })
Thay vì load toàn bộ hàng triệu bản ghi vào RAM cùng lúc, chunk() chỉ lấy 500 bản ghi mỗi lần truy vấn. Sau khi xử lý xong một batch, bộ nhớ sẽ được giải phóng trước khi lấy batch tiếp theo.
2. Eager Loading – Tránh N+1 Query
->with('customer')
Nạp sẵn dữ liệu quan hệ customer trong cùng một truy vấn, tránh tình trạng mỗi đơn hàng lại thực hiện thêm một câu query để lấy tên khách hàng.
3. Headers quan trọng
| Header | Giá trị | Mục đích |
|---|---|---|
Content-Type |
text/csv |
Xác định định dạng file |
Content-Disposition |
attachment; filename="orders.csv" |
Kích hoạt dialog tải file |
X-Accel-Buffering |
no |
Tắt buffering của Nginx để dữ liệu được truyền ngay lập tức |
Các trường hợp sử dụng phổ biến khác
1. Streaming JSON Lines (JSONL)
Phù hợp khi client cần xử lý từng bản ghi ngay khi nhận được:
Route::get('/stream-products', function () {
return response()->stream(function () {
Product::chunk(200, function ($products) {
foreach ($products as $product) {
echo json_encode($product) . "\n";
ob_flush();
flush();
}
});
}, 200, ['Content-Type' => 'application/x-ndjson']);
});
2. Server-Sent Events (SSE) – Cập nhật thời gian thực
SSE cho phép server đẩy dữ liệu xuống client theo thời gian thực (ví dụ: tiến trình xử lý, thông báo):
Route::get('/progress', function () {
return response()->stream(function () {
$steps = ['Đang chuẩn bị dữ liệu...', 'Đang xử lý...', 'Hoàn tất!'];
foreach ($steps as $i => $message) {
$progress = (int)(($i + 1) / count($steps) * 100);
echo "data: " . json_encode([
'message' => $message,
'progress' => $progress,
]) . "\n\n";
ob_flush();
flush();
sleep(1); // Giả lập tác vụ mất thời gian
}
}, 200, [
'Content-Type' => 'text/event-stream',
'Cache-Control' => 'no-cache',
'X-Accel-Buffering' => 'no',
]);
});
3. Streaming nội dung từ AI (LLM)
Kết hợp streaming với các API AI như OpenAI để hiển thị văn bản từng từ như ChatGPT:
Route::post('/ai-response', function (Request $request) {
return response()->stream(function () use ($request) {
$client = OpenAI::client(config('services.openai.key'));
$stream = $client->chat()->createStreamed([
'model' => 'gpt-4o',
'messages' => [['role' => 'user', 'content' => $request->input('prompt')]],
]);
foreach ($stream as $response) {
$text = $response->choices[0]->delta->content ?? '';
echo "data: " . json_encode(['text' => $text]) . "\n\n";
ob_flush();
flush();
}
}, 200, [
'Content-Type' => 'text/event-stream',
'Cache-Control' => 'no-cache',
'X-Accel-Buffering' => 'no',
]);
});
So sánh: Streaming vs Response thông thường
| Tiêu chí | Response thông thường | Streaming Response |
|---|---|---|
| Bộ nhớ sử dụng | Cao (load toàn bộ dữ liệu) | Thấp (xử lý từng phần) |
| Thời gian đến byte đầu tiên (TTFB) | Chậm | Nhanh |
| Nguy cơ timeout | Cao với dữ liệu lớn | Thấp |
| Độ phức tạp triển khai | Đơn giản | Trung bình |
| Phù hợp với | Dữ liệu nhỏ | Dữ liệu lớn, tác vụ dài |
Lưu ý khi triển khai lên môi trường production
Nginx
Cần thêm header X-Accel-Buffering: no hoặc cấu hình trong Nginx:
location /stream {
proxy_buffering off;
proxy_cache off;
}
PHP-FPM
Đảm bảo output_buffering được tắt trong php.ini hoặc dùng ini_set:
ini_set('output_buffering', 'off');
ini_set('zlib.output_compression', 'off');
Apache
Thêm vào .htaccess:
SetEnv no-gzip 1
SetEnv dont-vary 1
Kết luận
Laravel Streaming Responses là một công cụ mạnh mẽ và không thể thiếu khi xây dựng các tính năng như:
- Xuất file lớn (CSV, Excel với hàng triệu dòng)
- Xử lý dữ liệu thời gian thực (SSE, live progress)
- Tích hợp AI với phản hồi streaming từ LLM
- API trả về dữ liệu lớn mà không lo bộ nhớ
Bằng cách kết hợp response()->stream() với kỹ thuật chunk() để phân trang dữ liệu từ database, bạn có thể xử lý hàng triệu bản ghi một cách hiệu quả trong khi vẫn duy trì trải nghiệm người dùng tốt và hệ thống ổn định.