Как реализуешь индекатор онлайна?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Проектирование индикатора онлайн-статуса
Реализация индикатора онлайн-статуса зависит от требований к точности, производительности и архитектуре приложения. Рассмотрим ключевые аспекты и практические реализации на Go.
Основные подходы к определению онлайн-статуса
1. Polling (опрос)
Клиенты периодически отправляют запросы на сервер. Сервер отмечает последний запрос и считает пользователя онлайн, если запрос был недавно.
2. Heartbeat (пульс)
Клиенты отправляют специальные heartbeat-сообщения через WebSocket или long-polling соединения.
3. WebSocket соединения
Наиболее эффективный подход для real-time приложений, где соединение поддерживается постоянно.
Архитектурные компоненты
// Основные структуры для хранения статуса
type UserStatus struct {
UserID string `json:"user_id"`
IsOnline bool `json:"is_online"`
LastSeen time.Time `json:"last_seen"`
DeviceID string `json:"device_id"` // для поддержки multiple devices
}
type OnlineTracker struct {
mu sync.RWMutex
users map[string]*UserStatus
expiry time.Duration
cleanupTick *time.Ticker
}
Полная реализация на Go
package online
import (
"sync"
"time"
"context"
)
// OnlineTracker управляет статусами пользователей
type OnlineTracker struct {
mu sync.RWMutex
users map[string]*userState
expiry time.Duration
cleanupTick *time.Ticker
done chan bool
}
type userState struct {
userID string
lastActive time.Time
connections map[string]time.Time // deviceID -> last activity
}
func NewOnlineTracker(expiry time.Duration) *OnlineTracker {
ot := &OnlineTracker{
users: make(map[string]*userState),
expiry: expiry,
done: make(chan bool),
}
// Запускаем фоновую очистку устаревших записей
ot.cleanupTick = time.NewTicker(expiry / 2)
go ot.cleanupWorker()
return ot
}
// UpdateStatus обновляет статус пользователя
func (ot *OnlineTracker) UpdateStatus(userID, deviceID string) {
ot.mu.Lock()
defer ot.mu.Unlock()
now := time.Now()
if state, exists := ot.users[userID]; exists {
state.connections[deviceID] = now
state.lastActive = now
} else {
ot.users[userID] = &userState{
userID: userID,
lastActive: now,
connections: map[string]time.Time{deviceID: now},
}
}
}
// IsOnline проверяет онлайн-статус пользователя
func (ot *OnlineTracker) IsOnline(userID string) bool {
ot.mu.RLock()
defer ot.mu.RUnlock()
if state, exists := ot.users[userID]; exists {
return time.Since(state.lastActive) < ot.expiry
}
return false
}
// GetOnlineUsers возвращает список онлайн-пользователей
func (ot *OnlineTracker) GetOnlineUsers() []string {
ot.mu.RLock()
defer ot.mu.RUnlock()
onlineUsers := make([]string, 0)
now := time.Now()
for userID, state := range ot.users {
if now.Sub(state.lastActive) < ot.expiry {
onlineUsers = append(onlineUsers, userID)
}
}
return onlineUsers
}
// Удаляем неактивных пользователей
func (ot *OnlineTracker) cleanupWorker() {
for {
select {
case <-ot.cleanupTick.C:
ot.cleanup()
case <-ot.done:
return
}
}
}
func (ot *OnlineTracker) cleanup() {
ot.mu.Lock()
defer ot.mu.Unlock()
threshold := time.Now().Add(-ot.expiry)
for userID, state := range ot.users {
// Очищаем неактивные устройства
for deviceID, lastActive := range state.connections {
if lastActive.Before(threshold) {
delete(state.connections, deviceID)
}
}
// Если нет активных устройств, удаляем пользователя
if len(state.connections) == 0 || state.lastActive.Before(threshold) {
delete(ot.users, userID)
}
}
}
// Stop останавливает tracker
func (ot *OnlineTracker) Stop() {
ot.cleanupTick.Stop()
close(ot.done)
}
Интеграция с WebSocket
// WebSocket хендлер с поддержкой онлайн-статуса
func (h *Handler) HandleWebSocket(conn *websocket.Conn, userID string) {
defer func() {
// При отключении не сразу удаляем статус
// Ждем expiry времени на случай reconnection
time.AfterFunc(h.tracker.expiry, func() {
if !h.tracker.IsOnline(userID) {
// Уведомляем других пользователей
h.broadcastStatusChange(userID, false)
}
})
conn.Close()
}()
deviceID := generateDeviceID()
h.tracker.UpdateStatus(userID, deviceID)
h.broadcastStatusChange(userID, true)
// Heartbeat loop
heartbeat := time.NewTicker(30 * time.Second)
defer heartbeat.Stop()
for {
select {
case <-heartbeat.C:
h.tracker.UpdateStatus(userID, deviceID)
// Отправляем heartbeat обратно клиенту
conn.WriteJSON(map[string]string{"type": "heartbeat"})
case msg, ok := <-readChannel:
if !ok {
return
}
h.tracker.UpdateStatus(userID, deviceID)
// Обработка сообщений...
}
}
}
Продвинутые оптимизации
Распределенная архитектура:
// Использование Redis для кластерной среды
type RedisOnlineTracker struct {
client *redis.Client
expiry time.Duration
}
func (rot *RedisOnlineTracker) UpdateStatus(userID string) error {
key := fmt.Sprintf("online:%s", userID)
return rot.client.Set(key, "1", rot.expiry).Err()
}
Кэширование частых запросов:
// LRU кэш для частозапрашиваемых статусов
type CachedOnlineTracker struct {
tracker OnlineTrackerInterface
cache *lru.Cache
cacheTTL time.Duration
}
Критические аспекты реализации
- Точность vs Производительность: Слишком частые обновления увеличивают нагрузку
- Race Conditions: Используйте мьютексы или atomic операции для конкурентного доступа
- Распределенные системы: Для микросервисной архитектуры используйте Redis с ключами TTL
- Множественные устройства: Учитывайте, что пользователь может быть онлайн с нескольких устройств одновременно
- Grace Period: Добавьте задержку перед сменой статуса на "оффлайн" для unstable соединений
Мониторинг и метрики
// Экспорт метрик для Prometheus
func (ot *OnlineTracker) collectMetrics() {
onlineCount := len(ot.GetOnlineUsers())
metrics.OnlineUsers.Set(float64(onlineCount))
metrics.TotalUsers.Set(float64(len(ot.users)))
}
Заключение
Реализация индикатора онлайн-статуса требует баланса между точностью, производительностью и масштабируемостью. Для большинства приложений оптимальным является гибридный подход: WebSocket для real-time обновлений + heartbeat механизм + Redis для распределенного хранения. Важно предусмотреть grace period (обычно 15-30 секунд) перед переходом в оффлайн и реализовать эффективную очистку устаревших записей для предотвращения утечек памяти.