Кому
- Тем кто на
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 хранит данные в оперативной памяти, поэтому перебор с размером базы данных может плохо сказаться на работе всего сервера. Для оценки использовалась пустая база данных (выполнил
Аналогично для 65000 + 65000
FLUSHALL
перед этим) и вот этот небольшой скрипт на ruby. Для 9000 онлайн пользователей и 9000 онлайн анонимусов получилось так:- пустая база данных: 810.75K
- 18000 записей: 3.49M
Аналогично для 65000 + 65000
- 130000 записей: 18.66M
#UDP 1
Обёртка в
pipelined
заменена на использование опции ex: timeout
в вызове set
. Спасибо printercu за наводку. Небольшой тест [src:ruby] показал ощутимый прирост производительности.#UDP 2
Пара уточнений/рекомендаций:
- Использовать
before_filter
вместоafter_filter
— тогда вошедший пользователь будет видеть и себя в списке онлайн при первом (за ближайшие 10 минут) посещении. Впрочем тут выбор зависит только от Ваших потребностей/пожеланий. - Если идёт учёт анонимной аудитории и одновременно вошедших пользователей — то при входе/выходе пользователя, по большому счёту не плохо было бы, вычистить пользователя из противоположного списка. Например пользователь заходит на сайт (идёт запись по 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
Немножечко ссылок по теме (англ.):