Pull to refresh

Использование Redis EXPIRE для отслеживания онлайн-аудитории в Rails

Reading time4 min
Views13K

Кому


  • Тем кто на Ruby on Rails
  • Кто желает знать кто из пользователей онлайн, но ещё не задумывался как
  • Для кого не проблема использовать Redis (по причине хостинга например)


    Решение вопроса в сети пользователь или нет — это наверное как правило установка временной метки при обращении пользователя к приложению, а при необходимости узнать его (пользователя) текущий статус — сверка с этой временной меткой. Какой подход выбрать — решать Вам, но тот подход который предлагаю я — прост и не использует SQL базу данных, вместо этого используется Redis и одна из его встроенных возможностей — время жизни ключа (expire).



Собственно реализация


Инит


# config/initializers/redis.rb
    
$redis_onlines = Redis.new

Выше простейший подход, но я рекомендую следующий

# config/initializers/redis.rb

$redis_onlines = Redis.new path: "/tmp/redis.sock", db: 15, driver: :hiredis

  • path: "/tmp/redis.sock" — использовать socket-подключение, если это возможно
  • driver: :hiredis — драйвер hiredis быстрее
  • db: 15 — использовать определённую базу данных, по умолчанию используется нулевая, но я рекомендую оставить её для тестирования, прикладных задач, чего нибудь другого. Нет проблем в использовании именно нулевой базы данных — суть в том, чтобы она была строго определена под онлайн-пользователей и больше ни для чего.

Gemfile


# Gemfile

gem 'redis'
gem 'hiredis' # optional

Не забудьте запустить bundle

Устанавливаем в online


метод current_user
Метод current_user скорее всего уже используется Вами — это тот метод, который возвращает текущего пользователя или nil — если пользователь не вошёл.
def current_user
  @current_user ||= User.find_by_id( session[ :user_id ] )
end


# app/controllers/application_controller.rb
  after_filter :set_online
  # Для Rails 4 используйте:
  # after_action :set_online
  # после каждого запроса выполнить set_online

  private

    def set_online
      if !!current_user
        # не нужно значение, нужен только ключ
        $redis_onlines.set( current_user.id, nil, ex: 10*60 )
        # `ex: 10*60` - устанавливаем время жизни ключа - 10 минут, через 10 мину ключ удалиться
      end
    end

В сети?


    # app/models/user.rb
    
    def online?
      # если время жизни ключа истекло - то вернёт false, иначе true
      $redis_onlines.exists( self.id )
    end


Небольшой бонус — список онлайн пользователей


# app/cpntrollers/application_controller.rb

    def all_who_are_in_touch
      $redis_onlines.keys
      # => [ "123", "234", "1", "23" ]
      # вернёт массив с id онлайн пользователей
    end


На этом и всё

Небольшая переработка для anonymous



    Для отслеживания анонимных посетителей (тех кто не зарегистрировался/не вошёл) подход аналогичный, с небольшим дополнением.

Установка в online
    # app/controllers/application_controller.rb
    
def set_online
  if !!current_user
    # вошедшему пользователю к ключу добавляем префикс "user:" перед id
    $redis_onlines.set( "user:#{current_user.id}", nil, ex: 10*60 )
  else
    # не вошедшему пользователю добавляем префикс "ip:" и записываем его id адрес
    $redis_onlines.set( "ip:#{request.remote_ip}", nil, ex: 10*60 )
  end
end


в сети?
# app/models/user.rb

def online?
  $redis_onlines.exists( "user:#{self.id}" )
end


список пользователей в сети
# app/cpntrollers/application_controller.rb
    
  # все вошедшие пользователи онлайн (массив с их id)
  def all_signed_in_in_touch
    ids = []
    $redis_onlines.scan_each( match: 'user*' ){|u| ids << u.gsub("user:", "") }
    ids
  end

  # количество не вошедших пользователей онлайн
  def all_anonymous_in_touch
    $redis_onlines.scan_each( match: 'ip*' ).to_a.size
  end

  # количество всех пользователей онлайн
  def all_who_are_in_touch
    $redis_onlines.dbsize
  end



Ну и совсем чуть чуть по поводу размера базы данных


9000+9000
Redis хранит данные в оперативной памяти, поэтому перебор с размером базы данных может плохо сказаться на работе всего сервера. Для оценки использовалась пустая база данных (выполнил FLUSHALL перед этим) и вот этот небольшой скрипт на ruby. Для 9000 онлайн пользователей и 9000 онлайн анонимусов получилось так:

  1. пустая база данных: 810.75K
  2. 18000 записей: 3.49M

Аналогично для 65000 + 65000

  1. 130000 записей: 18.66M



#UDP 1


    Обёртка в pipelined заменена на использование опции ex: timeout в вызове set. Спасибо printercu за наводку. Небольшой тест [src:ruby] показал ощутимый прирост производительности.

#UDP 2


    Пара уточнений/рекомендаций:
  1. Использовать before_filter вместо after_filter — тогда вошедший пользователь будет видеть и себя в списке онлайн при первом (за ближайшие 10 минут) посещении. Впрочем тут выбор зависит только от Ваших потребностей/пожеланий.
  2. Если идёт учёт анонимной аудитории и одновременно вошедших пользователей — то при входе/выходе пользователя, по большому счёту не плохо было бы, вычистить пользователя из противоположного списка. Например пользователь заходит на сайт (идёт запись по ip), а затем авторизуется (вторая запись по id) — в итоге ближайшее время (условные 10 минут) один пользователь будет считаться за двоих. Пример реализации в спойлере ниже.

очистка противоположного списка
# app/cpntrollers/session_controller.rb
# это пример
# авторизация - действие create
# выход - destroy
  before_filter :clear_from_signed_in_touch, only: :destroy
  before_filter :clear_from_anonymous_in_touch, only: :create

  # ...

  private

    # при входе - удаляем пользователя из записи по ip
    def clear_from_anonymous_in_touch
      $redis_onlines.del( "ip:#{request.remote_ip}" )
    end

    # в примере id вошедшего пользователя храниться в сессии - session[:user_id]
    # при выходе - удаляем пользователя из записи по id
    def clear_from_signed_in_touch
      $redis_onlines.del( "user:#{session[:user_id]}" )
    end




Немножечко ссылок по теме (англ.):

Tags:
Hubs:
Total votes 7: ↑5 and ↓2+3
Comments9

Articles