Уявіть: конференція на 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 день — отримайте детальний план і точні строки. Замовте консультацію, щоб переконатися, що ваше голосування працюватиме без збоїв при будь-якому навантаженні.







