הטמעת מרכז התראות בזמן אמת
תארו לעצמכם שמוצר ה-SaaS שלכם מעבד מאות אירועים בדקה – הזמנות חדשות, תגובות, סטטוסי משימות. משתמשים מתלוננים שהם מפספסים התראות חשובות, ופאנל הניהול עמוס. בפרויקט אחד, 20,000 משתמשים יצרו בו-זמנית 500 התראות בשנייה, וללא ארכיטקטורה מתאימה, המערכת קרסה. לאחר הטמעת הפתרון שלנו, עומס השרת ירד ב-70%. פתרנו בעיה זו על ידי פריסת מרכז התראות בזמן אמת המבוסס על Laravel + WebSocket + React. מתחת למכסה המנוע: מודל מסד נתונים אופטימלי, אספקה אסינכרונית ו-UX מחושב היטב.
בעיות שמרכז ההתראות פותר
פתרונות Polling מסורתיים יוצרים עומס יתר על השרת: שאילתת מסד נתונים כל 5 שניות. עם 1,000 משתמשים פעילים, מדובר ב-200 RPS רק לבדיקת התראות. WebSocket (Laravel Echo + Pusher) מספק מודל Push: התראות מגיעות מיידית ללא בקשות נוספות. זה מפחית את ה-TTFB פי 3–4 ומשפר את Core Web Vitals. לדוגמה, לאחר הטמעה בפרויקט עם 5,000 DAU, תלונות על עיכובים בהתראות ירדו ב-90%. בעיה נוספת היא התראות שאבדו עקב ארכיטקטורה שבורה: הלקוח עלול לפספס אירוע אם חיבור ה-WebSocket נופל. הפתרון שלנו משתמש בתורי Laravel עם ניסיונות חוזרים אוטומטיים ומנגנון אישור אספקה.
בחירת דרייבר WebSocket
| דרייבר | אירוח | קנה מידה | חביון | תכונות |
|---|---|---|---|---|
| Pusher | SaaS | מובנה | <20ms | התחלה מהירה, תשלום חודשי קבוע |
| Soketi | אירוח עצמי | אופקי דרך Redis | <50ms | חינם, שליטה מלאה |
| Laravel Reverb | אירוח עצמי | אופקי דרך Redis | <30ms | קוד פתוח, תאימות ל-Laravel |
Pusher הוא הבחירה הטובה ביותר להתחלה מהירה, Soketi ו-Reverb לשליטה מלאה וחיסכון בעלויות עם משתמשים רבים. הבחירה תלויה בדרישות SLA: SaaS מספק עלות צפויה, אירוח עצמי מוריד הוצאות תשתית. לפרויקטים קטנים, Pusher מספיק; למערכות ארגוניות עם עשרות אלפי חיבורים, Soketi עדיף.
Laravel Broadcasting מאפשר התראות Push בזמן אמת דרך WebSocket, ומפחית חביון לפחות מ-50ms.
| סוג התראה | תיאור | דוגמת נתונים |
|---|---|---|
| מערכת | שגיאה, אזהרה | 'הרישיונות שלך פגו' |
| חברתי | לייקים, תגובות | 'משתמש X השאיר תגובה' |
| אירוע עסקי | הזמנה, תשלום | 'הזמנה חדשה #123' |
מבנה מסד הנתונים ואינדקסים
למה אינדקסים בטבלת ההתראות חשובים?
ללא אינדקסים, שאילתות להתראות שלא נקראו סורקות את כל הטבלה. בטבלה עם מיליוני שורות, זה מוביל ל-timeouts. אנו משתמשים באינדקס מכסה על (user_id, read_at, created_at DESC), שמאיץ מיון וסינון פי 10. טבלת ההתראות יכולה להגיע ל-10 מיליון שורות, וללא אינדקסים, שאילתות לוקחות 5–10 שניות במקום 50 ms. מבנה זה מבטיח inserts ו-selects מהירים, כשהאינדקס מכסה 95% מהשאילתות.
CREATE TABLE notifications (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
type VARCHAR(100) NOT NULL,
icon VARCHAR(50),
title VARCHAR(255),
body TEXT,
url VARCHAR(500),
data JSONB NOT NULL DEFAULT '{}',
read_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX ON notifications(user_id, read_at, created_at DESC);
אופציונלית, ניתן להוסיף אינדקס על CREATE TABLE notifications ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, type VARCHAR(100) NOT NULL, icon VARCHAR(50), title VARCHAR(255), body TEXT, url VARCHAR(500), data JSONB NOT NULL DEFAULT '{}', read_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX ON notifications(user_id, read_at, created_at DESC); לסינון לפי קטגוריה.
Laravel: נקודות קצה API ושידור
class NotificationController extends Controller
{
public function index(Request $request): JsonResponse
{
$notifications = auth()->user()->notifications()
->latest()
->limit(50)
->get();
return response()->json([
'notifications' => NotificationResource::collection($notifications),
'unread_count' => $notifications->whereNull('read_at')->count(),
]);
}
public function markRead(Request $request): JsonResponse
{
$query = auth()->user()->notifications()->whereNull('read_at');
if ($request->id) {
$query->where('id', $request->id);
}
$query->update(['read_at' => now()]);
return response()->json(['success' => true]);
}
public function send(User $user, array $data): void
{
$notification = $user->notifications()->create($data);
broadcast(new NotificationCreatedEvent($user->id, $notification))->toOthers();
}
} Laravel Echo + WebSocket: אינטגרציית לקוח
// hooks/useNotifications.ts
import Echo from 'laravel-echo';
import Pusher from 'pusher-js';
window.Pusher = Pusher;
const echo = new Echo({
broadcaster: 'pusher',
key: import.meta.env.VITE_PUSHER_KEY,
cluster: import.meta.env.VITE_PUSHER_CLUSTER,
forceTLS: true,
});
export function useNotifications(userId: number) {
const [notifications, setNotifications] = useState<Notification[]>([]);
const [unreadCount, setUnreadCount] = useState(0);
useEffect(() => {
api.get('/api/notifications').then(({ data }) => {
setNotifications(data.notifications);
setUnreadCount(data.unread_count);
});
const channel = echo.private(`notifications.${userId}`)
.listen('.NotificationCreated', (event: { notification: Notification }) => {
setNotifications(prev => [event.notification, ...prev].slice(0, 50));
setUnreadCount(c => c + 1);
if (Notification.permission === 'granted') {
new Notification(event.notification.title ?? 'Новое уведомление', {
body: event.notification.body ?? undefined,
icon: '/icon-192.png',
});
}
});
return () => channel.stopListening('.NotificationCreated');
}, [userId]);
const markAllRead = async () => {
await api.post('/api/notifications/mark-read');
setNotifications(prev => prev.map(n => ({ ...n, read_at: new Date().toISOString() })));
setUnreadCount(0);
};
return { notifications, unreadCount, markAllRead };
} React: רכיב UI
function NotificationBell({ userId }: { userId: number }) {
const { notifications, unreadCount, markAllRead } = useNotifications(userId);
const [isOpen, setIsOpen] = useState(false);
return (
<div className="notification-bell">
<button
onClick={() => setIsOpen(!isOpen)}
aria-label={`${unreadCount} непрочитанных уведомлений`}
aria-expanded={isOpen}
aria-haspopup="true"
>
🔔
{unreadCount > 0 && (
<span className="badge" aria-hidden>{unreadCount > 99 ? '99+' : unreadCount}</span>
)}
</button>
{isOpen && (
<div className="notification-panel" role="dialog" aria-label="Уведомления">
<header>
<h2>Уведомления</h2>
{unreadCount > 0 && (
<button onClick={markAllRead}>Прочитать все</button>
)}
</header>
<ul>
{notifications.length === 0 && <li className="empty">Нет уведомлений</li>}
{notifications.map(notification => (
<li key={notification.id} className={notification.read_at ? 'read' : 'unread'}>
{notification.url ? (
<a href={notification.url}>{notification.title}</a>
) : (
<span>{notification.title}</span>
)}
<time dateTime={notification.created_at}>{timeAgo(notification.created_at)}</time>
{notification.body && <p>{notification.body}</p>}
</li>
))}
</ul>
</div>
)}
</div>
);
} תהליך עבודה ולוחות זמנים
איך מובטחת יציבות ההתראות בזמן אמת?
אנו מבטיחים זמינות של 99.9% למרכז ההתראות. אנו משתמשים בתורים (Laravel Queues) עם לוגיקת ניסיון חוזר על כשלים. בצד הלקוח – חיבור מחדש אוטומטי דרך Echo (Reconnect). עבור Pusher – גיבוי ל-SSE. עבור אירוח עצמי – ניטור דרך Laravel Horizon והתראות Telegram. יש לנו ניסיון של שנים עם יישומי web מורכבים ויותר מ-20 הטמעות של מרכזי התראות. תהליך שקוף מבטיח שתקבלו פתרון עובד בתוך לוחות הזמנים המוסכמים.
- ניתוח — אנו בוחנים דרישות, עומס ובוחרים את דרייבר ה-WebSocket.
- עיצוב — עיצוב סכמת DB, API ואירועים.
- הטמעה — כתיבת קוד, בדיקות על staging.
- בדיקות — בדיקות עומס (10,000+ חיבורים), אימות גיבוי.
- פריסה — הגדרת סביבת production, ניטור.
מה כלול
- תיעוד: ארכיטקטורה, סכמת DB, רצף אירועים, הגדרת סביבה.
- קוד: backend (Laravel), frontend (React), תצורת תורים ושידור.
- גישה ל-repository ו-CI/CD.
- הדרכת צוות: 1–2 פגישות מקוונות.
- תמיכה: חודש אחד לאחר הפריסה.
ההשקעה במרכז התראות מחזירה את עצמה דרך הפחתת עומס והגדלת נאמנות המשתמשים. כדי לקבל פתרון מוכן, צרו קשר — אנו נבצע בדיקת פרויקט ונציע ארכיטקטורה אופטימלית. בקשו ייעוץ מהמהנדסים שלנו.
לוח זמנים להטמעה
אינטגרציה בסיסית: 2–3 ימים. WebSocket באירוח עצמי (Soketi/Laravel Reverb): +יום אחד. אם נדרשת לוגיקת קיבוץ או סינון מותאמת אישית, לוח הזמנים נדון בנפרד.







