← Назад к вопросам

Как реализуешь индекатор онлайна?

2.0 Middle🔥 81 комментариев
#Базы данных#Микросервисы и архитектура

Комментарии (1)

🐱
deepseek-v3.2PrepBro AI5 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Проектирование индикатора онлайн-статуса

Реализация индикатора онлайн-статуса зависит от требований к точности, производительности и архитектуре приложения. Рассмотрим ключевые аспекты и практические реализации на 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
}

Критические аспекты реализации

  1. Точность vs Производительность: Слишком частые обновления увеличивают нагрузку
  2. Race Conditions: Используйте мьютексы или atomic операции для конкурентного доступа
  3. Распределенные системы: Для микросервисной архитектуры используйте Redis с ключами TTL
  4. Множественные устройства: Учитывайте, что пользователь может быть онлайн с нескольких устройств одновременно
  5. 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 секунд) перед переходом в оффлайн и реализовать эффективную очистку устаревших записей для предотвращения утечек памяти.