הצבעות וסקרים בזמן אמת לאתר שלך
תארו לעצמכם: כנס עם 10,000 משתתפים, המארגנים מפעילים הצבעה, והתוצאות מתעדכנות כל 30 שניות באמצעות שאילתות AJAX רגילות. משתמשים מתלוננים על עיכובים, השרת קורס תחת 10,000 בקשות בשנייה. נשמע מוכר? בחירת הצבעה בזמן אמת פותרת שתי בעיות: אספקת תוצאות מיידית והפחתת עומס על השרת. יישמנו מערכות כאלה עבור 30+ פרויקטים — מסקרים ארגוניים ועד שידורים חיים בקנה מידה גדול.
למה SSE במקום שאילתות?
שאילתות פירושן N בקשות בשנייה, כאשר N הוא מספר המשתמשים. עם 500 משתמשים — 500 בקשות בשנייה מבוזבזות. SSE או WebSocket נותנים חיבור אחד לכל משתמש, דרכו השרת שולח נתונים רק כשהם משתנים. להצבעות, שני מנגנונים עובדים: SSE (Server-Sent Events) ו-WebSocket. אפשרות שלישית — שאילתות — איננו שוקלים: בקשה אחת בשנייה עבור 500 משתמשים מחוברים היא עומס של 500 בקשות בשנייה רק כדי לבדוק "שום דבר לא השתנה." SSE הוא זרם חד-כיווני מהשרת ללקוח, המתואר במפרט EventSource. להצבעות זה מספיק: ההצבעה נשלחת באמצעות POST רגיל, התוצאה מגיעה דרך SSE. WebSocket מוצדק אם אתם צריכים משוב מיידי (אנימציה "ההצבעה שלך התקבלה") או אלמנטים אינטראקטיביים נוספים.
| פרמטר | SSE | WebSocket |
|---|---|---|
| כיוון | חד-כיווני (שרת → לקוח) | דו-כיווני |
| מורכבות יישום | נמוכה (EventSource טבעי) | בינונית (דורש שרת WebSocket) |
| תשתית | ללא sticky sessions | דורש sticky session או שרת נפרד |
| ביצועים | עד 10,000 חיבורים לכל עובד PHP | עד 100,000 חיבורים על Node.js |
| תמיכת דפדפנים | כל המודרניים, מלבד IE | כל המודרניים |
איך למנוע הצבעות כפולות?
למשתמשים מורשים — אילוץ ייחודיות (option_id, user_id) ובדיקה ב-controller. להצבעות אנונימיות — הגנה באמצעות IP + טביעת אצבע. טביעת האצבע נוצרת בצד הלקוח (ספריית 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: $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(); — זה מונע שליחה בו-זמנית מחשבון אחד.
איך להגדיל הצבעות לאלפי משתתפים?
יישום PHP עם SSE שומר על החיבור פתוח. 1,000 משתמשים מחוברים = 1,000 עובדי PHP. זה יקר. פתרון: העברת השידור דרך Pusher או Laravel Echo Server (socket.io). אז אין יותר צורך ב-SSE controller — הלקוח נרשם לערוץ, השרת מפרסם אירוע Cache::lock('vote:'.$poll->id.':'.$userId, 10)->get() ל-Redis, Laravel Echo משדר לכל המנויים.
// После записи голоса broadcast(new PollUpdated($poll->id, $counts))->toOthers(); Echo.channel(`poll.${pollId}`) .listen('PollUpdated', ({ counts }) => updateBars(counts)); ארכיטקטורה זו מתמודדת עם מאות אלפי חיבורים על תהליך Node.js יחיד. לניטור, השתמשו ב-Laravel Horizon: הוא מציג את מספר עובדי ה-SSE הפעילים ואת זמן התגובה. זה חוסך עד 70% מעלויות תשתית השרת בהשוואה לעובדי 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: poll.updated.
צד הלקוח ושליחת הצבעות
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
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',
]);
});
// После записи голоса broadcast(new PollUpdated($poll->id, $counts))->toOthers(); היא כותרת חובה בשימוש ב-Nginx כפרוקסי; אחרת נתונים יצטברו ב-buffer.
איך לבדוק הצבעה בזמן אמת?
השתמשו בכלים: Postman לשליחת הצבעות, k6 לבדיקת עומסים, קונסולת הדפדפן לבדיקת חיבורי SSE. תרחישים עיקריים: 1) בדיקת קבלת עדכונים לאחר הצבעה; 2) הדמיית הצבעות בו-זמניות מאלף משתמשים; 3) בדיקת עמידות בנפילת חיבור (SSE מתחבר מחדש אוטומטית).
טעויות נפוצות ופתרונות
- שאילתות N+1: בעת שליפת רשימת הצבעות עם אפשרויות בטעינה עצלה. השתמשו ב-
Echo.channel(`poll.${pollId}`) .listen('PollUpdated', ({ counts }) => updateBars(counts));ב-Eloquent. - חסר
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) );: ללא כותרת זו, Nginx מאגר את זרם ה-SSE, ומשתמשים רואים נתונים בנתחים. - התעלמות מ-
HINCRBY poll:42:counts 1 1: בלעדיה, עובדי PHP ממשיכים להיתקע, וצורכים זיכרון. - אי שימוש ב-Redis למונים: COUNT ישיר מה-DB בכל עדכון יוצר עומס. הגדלות Redis מהירות בהרבה.
לוחות זמנים לפיתוח
| שלב | זמן |
|---|---|
| הצבעה בסיסית (SSE, משתמשים מורשים) | 2–3 ימים |
| הצבעה אנונימית + מניעת כפילויות | +יום אחד |
| סקרים עם אפשרויות מרובות + היסטוריה | +יום אחד |
| הגדלה דרך Pusher/Echo | +יומיים |
| ממשק ניהול | 2–3 ימים |
אנו מבטיחים תקופה נטולת באגים של 30 יום לאחר המסירה, מספקים תיעוד ומכשירים את הצוות שלכם. כדי להעריך את הפרויקט שלכם, צרו קשר — קבלו תוכנית מפורטת ולוח זמנים מדויק. בקשו ייעוץ כדי להבטיח שההצבעה שלכם תפעל בצורה חלקה תחת כל עומס.







