Уявіть: конференція на 10 000 учасників, організатори запускають голосування, а результати оновлюються раз на 30 секунд через звичайний AJAX-polling. Користувачі скаржаться на затримки, сервер падає від 10 000 запитів на секунду. Знайома ситуація? Обираючи real-time голосування, ви вирішуєте два завдання: миттєва доставка результатів та зниження навантаження на сервер. Наша команда має понад 5 років досвіду у розробці високонавантажених веб-систем і реалізувала такі рішення для 30+ проєктів — від корпоративних опитувань до масштабних прямих ефірів. Ми пропонуємо розробку систем голосування під ключ: від проектування до розгортання.
Чому SSE, а не polling?
Polling — це N запитів на секунду, де N — кількість користувачів. При 500 користувачах — 500 req/s даремно. SSE або WebSocket дають одне з'єднання на користувача, через яке сервер надсилає дані лише при зміні. Для голосувань підходять два механізми: SSE (Server-Sent Events) та WebSocket. Третій варіант — polling — не розглядаємо: 1 запит на секунду на 500 одночасних користувачів — це 500 req/s навантаження лише заради перевірки «нічого не змінилося». SSE — однонаправлений потік від сервера до клієнта, описаний у специфікації EventSource. Для опитувань в реальному часі цього достатньо: голос надсилається звичайним POST, результат приходить через SSE. WebSocket виправданий, якщо потрібен негайний зворотний зв'язок (анімація «ваш голос прийнято») або додаткові інтерактивні елементи. Зазначимо, що SSE у 3 рази простіший у реалізації, ніж WebSocket, при цьому покриває той самий базовий функціонал.
| Параметр | SSE | WebSocket |
|---|---|---|
| Напрямок | Однонаправлений (сервер → клієнт) | Двонаправлений |
| Складність реалізації | Низька (нативний EventSource) | Середня (потрібен WebSocket-сервер) |
| Інфраструктура | Не потребує sticky sessions | Потребує sticky session або окремий сервер |
| Продуктивність | До 10 000 з'єднань на один PHP-воркер | До 100 000 з'єднань на Node.js |
| Підтримка браузерів | Всі сучасні, крім IE | Всі сучасні |
Як запобігти дублюванню голосів?
Для авторизованих користувачів — unique constraint (option_id, user_id) та перевірка в контролері. Для анонімних голосувань — захист через IP + fingerprint. Fingerprint генерується на фронті (бібліотека fingerprintjs) і передається в заголовку. Це не абсолютний захист, але достатній для більшості випадків.
$fingerprint = $request->header('X-Client-Fingerprint');
$alreadyVoted = PollVote::where('poll_id', $poll->id)
->where(function ($q) use ($request, $fingerprint) {
$q->where('ip', $request->ip())
->orWhere('fingerprint', $fingerprint);
})->exists();
Додатково можна використовувати Redis-блокування: Cache::lock('vote:'.$poll->id.':'.$userId, 10)->get() — це запобіжить одночасному надсиланню з одного акаунту.
Як масштабувати голосування на тисячі учасників?
PHP-застосунок з SSE тримає з'єднання відкритим. 1 000 одночасних користувачів = 1 000 PHP-воркерів. Це дорого. Рішення: винести broadcast через Pusher або Laravel Echo Server (socket.io). Тоді SSE-контролер більше не потрібен — клієнт підписується на канал, сервер публікує подію poll.updated в Redis, Laravel Echo транслює всім підписникам. Наприклад, для проєкту з 1000 учасників використання Pusher замість прямих SSE-воркерів може зменшити витрати на сервери з $500 до $150 на місяць — економія понад 70%.
// Після запису голосу
broadcast(new PollUpdated($poll->id, $counts))->toOthers();
Echo.channel(`poll.${pollId}`)
.listen('PollUpdated', ({ counts }) => updateBars(counts));
Така архітектура тримає сотні тисяч підключень на одному процесі Node.js. Для моніторингу використовуйте Laravel Horizon: він показує кількість активних SSE-воркерів та час відгуку. Pusher для голосування витримує навантаження у 2-3 рази більше, ніж власний SSE-воркер, при тих же витратах.
Схема даних та оптимізація
Схема бази даних
CREATE TABLE polls (
id BIGSERIAL PRIMARY KEY,
title VARCHAR(500) NOT NULL,
is_multiple BOOLEAN NOT NULL DEFAULT false,
is_active BOOLEAN NOT NULL DEFAULT true,
ends_at TIMESTAMP,
created_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE TABLE poll_options (
id BIGSERIAL PRIMARY KEY,
poll_id BIGINT NOT NULL REFERENCES polls(id) ON DELETE CASCADE,
label VARCHAR(255) NOT NULL,
position SMALLINT NOT NULL DEFAULT 0
);
CREATE TABLE poll_votes (
id BIGSERIAL PRIMARY KEY,
option_id BIGINT NOT NULL REFERENCES poll_options(id),
user_id BIGINT REFERENCES users(id),
ip INET,
voted_at TIMESTAMP NOT NULL DEFAULT NOW(),
UNIQUE(option_id, user_id)
);
Агрегація рахується через materialized view або прямим COUNT. При піковому навантаженні (прямий ефір, 5 000+ учасників) краще зберігати лічильники окремо та інкрементувати через Redis: HINCRBY poll:42:counts 1 1.
Клієнтська частина та надсилання голосу
const pollId = 42;
const source = new EventSource(`/api/polls/${pollId}/stream`);
source.onmessage = (event) => {
const { counts } = JSON.parse(event.data);
updateBars(counts);
};
source.onerror = () => {
console.warn('SSE reconnecting...');
};
function updateBars(counts) {
const total = Object.values(counts).reduce((a, b) => a + Number(b), 0);
document.querySelectorAll('[data-option-id]').forEach(el => {
const id = el.dataset.optionId;
const pct = total > 0 ? Math.round((counts[id] || 0) / total * 100) : 0;
el.querySelector('.bar').style.width = pct + '%';
el.querySelector('.label').textContent = pct + '%';
});
}
async function vote(optionId) {
const resp = await fetch(`/api/polls/${pollId}/vote`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrfToken },
body: JSON.stringify({ option_id: optionId }),
});
if (resp.status === 409) {
showMessage('Ви вже голосували');
}
}
Як реалізувати SSE-ендпоінт на Laravel
Кроки реалізації:
- Створіть маршрут, що повертає streaming response.
- У контролері в циклі отримуйте поточну статистику через Redis або COUNT.
- Відправляйте дані у форматі SSE:
echo "data: {$data}\n\n";. - Додайте обов'язковий заголовок
X-Accel-Buffering: noдля Nginx. - Перевіряйте
connection_aborted()для коректного завершення.
Route::get('/api/polls/{poll}/stream', function (Poll $poll) {
return response()->stream(function () use ($poll) {
while (true) {
if (connection_aborted()) break;
$counts = PollVote::selectRaw('option_id, COUNT(*) as votes')
->whereIn('option_id', $poll->options->pluck('id'))
->groupBy('option_id')
->pluck('votes', 'option_id');
$data = json_encode(['counts' => $counts, 'ts' => now()->timestamp]);
echo "data: {$data}\n\n";
ob_flush();
flush();
sleep(2);
}
}, 200, [
'Content-Type' => 'text/event-stream',
'Cache-Control' => 'no-cache',
'X-Accel-Buffering' => 'no',
]);
});
Як тестувати real-time голосування?
Використовуйте інструменти: Postman для надсилання голосів, k6 для навантажувального тестування, вбудовану консоль браузера для перевірки SSE-з'єднань. Основні сценарії: 1) перевірка отримання оновлень після голосування; 2) симуляція одночасних голосів від тисячі користувачів; 3) перевірка стійкості при обриві з'єднання (SSE автоматично перепідключається).
Типові помилки та їх вирішення
- N+1 запити: при отриманні списку голосів з підвантаженням опцій через lazy loading. Використовуйте
with('options')в Eloquent. - Відсутність
X-Accel-Buffering: без цього заголовка Nginx буферизує SSE-потік, і користувачі бачать дані пачками. - Ігнорування
connection_aborted(): без цього PHP-воркери продовжують висіти, споживаючи пам'ять. - Невживання Redis для лічильників: прямий COUNT з БД при кожному оновленні створює навантаження. Redis інкременти в рази швидші.
Що входить у роботу
- Технічне завдання та узгодження архітектури
- Розробка системи голосування під ключ
- Документація з інтеграції для вашої команди
- Навчання адміністраторів та розробників
- Налаштування моніторингу (Laravel Horizon або аналог)
- 30-денна гарантія на відсутність багів та підтримка після запуску
Строки розробки
| Етап | Час |
|---|---|
| Базове голосування (SSE, авторизовані) | 2–3 дні |
| Анонімне голосування + anti-duplicate | +1 день |
| Багатоваріантні опитування + історія | +1 день |
| Масштабування через Pusher/Echo | +2 дні |
| Адміністративний інтерфейс | 2–3 дні |
Ми пропонуємо реалізацію голосування на сайті з урахуванням усіх сучасних вимог. Наша команда має 5+ років досвіду та 30+ успішних проєктів. Пишіть нам, і ми оцінимо ваш проект за 1 день — отримайте детальний план і точні строки. Замовте консультацію, щоб переконатися, що ваше голосування працюватиме без збоїв при будь-якому навантаженні.







