Redis — это хранилище значений ключей, которое отличается от других, например memcached , тем, что имеет встроенную поддержку структур данных, таких как списки, наборы и хэши, и может сохранять данные на диске. Как таковой, он весьма полезен как хранилище кеша, так и как полноценное хранилище данных NoSQL. В этой статье мы рассмотрим пример базового использования, чтобы узнать, как использовать Redis в приложении Rails.
Основной пример
Представьте, что мы написали простую платформу для блогов в Rails. Мы используем MySQL в качестве нашей основной базы данных, где мы храним весь контент постов, комментарии и учетные записи пользователей. Допустим, он размещен на myrailsblog.com. Мы получаем регулярный трафик непосредственно на сообщения, благодаря нашей стратегии социальных сетей, которая меняет правила игры. Есть прямые ссылки на эти посты по всему интернету.
Мы не без ума от того, что URL-адрес сообщения выглядит следующим образом: http://myrailsblog.com/321. Помимо того, что это немного уродливо и предоставляет внутренние идентификаторы, мы могли бы получить лучший рейтинг в поисковых системах, если бы внедрили слагов для URL-адресов записей. Вместо этого мы хотели бы, чтобы URL выглядел следующим образом: http://myrailsblog.com/using-redis-with-rails.
Поэтому мы решили, что теперь посты будут содержать слагов, которые автоматически генерируются из заголовка. Мы найдем сообщения по слагу, а не по идентификатору. Когда создается новое сообщение, оно создает слаг, который сохраняется в новом столбце нашей таблицы сообщений. Тем не менее, наша платформа позволяет редактировать заголовки сообщений, и, кроме того, мы хотели бы, чтобы авторы могли настраивать слаг в случае, если сгенерированный автоматически окажется нежелательным по какой-либо причине. Это означает, что URL публикации может измениться после публикации. Что происходит, когда кто-то переходит на старый URL? Вместо того, чтобы неловко показывать 404, наша платформа должна найти правильный пост от старого слага. Это означает, что все старые слизни должны продолжать ссылаться на пост.
Наш естественный подход в качестве разработчиков Rails к поиску поста по его слагу — реализовать что-то вроде этого:
# app/controllers/posts_controller.rb | |
class PostsController < ApplicationController | |
before_filter :find_post | |
protected | |
def find_post | |
unless @post = Post.where(:slug => params[:id]).first | |
# what do we do here? | |
end | |
end | |
end | |
# app/models/post.rb | |
class Post < ActiveRecord::Base | |
def to_param | |
self.slug | |
end | |
end |
Вопросы начинают возникать, когда мы думаем о бите «что мы здесь делаем?». Концептуально, нам нужно сравнить параметр: id со старыми слагами поста, чтобы увидеть, совпадает ли какой-либо из них. Один из возможных подходов — создать модель Slug
и установить для Post
s значение have_many :slugs
.
Тем не менее, мы можем решить эту проблему немного более элегантно с помощью Redis, сопоставляя слагов для публикации идентификаторов. Мы можем использовать тип хеш-данных для хранения всех слагов под одним основным ключом. Мы получим O (1) время поиска, и нам не нужно будет переносить базу данных. В конце концов, поиск сообщения будет выглядеть так:
# app/controllers/post_controller.rb | |
class PostsController < ApplicationController | |
before_filter :find_post | |
protected | |
def find_post | |
@post = Post.find(Slug[params[:id]]) | |
rescue ActiveRecord::RecordNotFound | |
redirect_to root_path | |
end | |
end |
Добавление Redis
Начать работу с Redis так же просто, как собрать сервер, запустить его и подключиться к нему. Загрузка и установка Redis выходит за рамки этой статьи, но здесь есть подробные инструкции. После успешной установки его можно запустить, открыв новое окно терминала и введя redis-server
.
Теперь, когда сервер запущен, нам нужно дать нашему приложению пообщаться с ним. Для начала нам нужно установить гем redis. С помощью bundler это так же просто, как добавить следующее в Gemfile и запустить bundle install
:
gem ‘redis’ | |
gem ‘redis-namespace’ |
Затем нам нужно подключиться к серверу и сохранить это соединение как глобальный ресурс. Кроме того, мы собираемся использовать гем «redis-namespace» для организации всего в одном пространстве имен всего приложения. Это очень поможет в будущем, когда несколько приложений используют один сервер Redis. Создайте файл с именем «redis.rb» в вашей папке config / initializers следующим образом:
$redis = Redis::Namespace.new(«my_app», :redis => Redis.new) |
Вот и все. Теперь мы можем вводить команды Redis непосредственно в глобальную переменную $redis
. Давайте проверим это, открыв консоль Rails и выполнив следующие команды:
> $redis.set(«foo«, «bar«) | |
=> «OK« | |
> $redis.get(«foo«) | |
=> «bar« | |
> $redis.get(«baz«) | |
=> nil | |
> $redis.del(«foo«) | |
=> 1 | |
> $redis.get(«foo«) | |
=> nil | |
> |
Здорово. Мы можем видеть, что методы экземпляра класса Redis такие же, как команды Redis, найденные здесь . Для простых операций get / set мы также можем использовать обозначение массива:
> $redis[«foo«] = «bar« | |
=> «bar« | |
> $redis[«foo«] | |
=> «bar« | |
> $redis[«baz«] | |
=> nil | |
> $redis.del(«foo«) | |
=> 1 | |
> $redis[«foo«] | |
=> nil | |
> |
Теперь мы готовы копнуть немного глубже.
Модель Slug
Fetching
У модели Slug есть одна главная цель: превратить Slug в почтовый идентификатор. Мы собираемся использовать тип хеш-данных, чтобы избежать загрязнения пространства ключей кучей слагов. Таким образом, все slugs будут находиться под одним ключом, «slugs». Наш первый проход в app/models/slug.rb
выглядит следующим образом:
class Slug | |
class << self | |
def [](slug) | |
redis.hget(hash, slug) | |
end | |
private | |
def redis | |
$redis | |
end | |
def hash | |
«post_ids» | |
end | |
end | |
end |
Мы просто выбираем значение определенного слага в пределах хеша «slugs». Если ключ не существует, Redis возвращает nil
, что заставит наш PostsController
перенаправить на корневой путь. Извлечение легко, но мы должны иметь возможность хранить сопоставления слагов / идентификаторов, чтобы это имело какой-либо эффект.
хранения
Хранить значения достаточно просто. Мы можем добавить PostObserver
в наше приложение с помощью следующего кода:
class PostObserver | |
def after_save(post) | |
Slug[post.slug] = post.id.to_s | |
return true | |
end | |
end |
Затем мы можем изменить нашу модель Slug
чтобы сделать возможным сопоставление slug с идентификатором поста:
class Slug | |
class << self | |
def [](slug) | |
redis.hget(hash, slug) | |
end | |
def []=(slug, id) | |
redis.hset(hash, slug, id) | |
end | |
private | |
def redis | |
$redis | |
end | |
def hash | |
«post_ids» | |
end | |
end | |
end |
Удаление
Последнее, что нам нужно сделать, это очистить неиспользованные слагов, когда сообщение уничтожено. Это потому, что нам нужно будет проверить уникальность слагов против всех слагов. Если слаг указывает на сообщение, которого больше не существует, он будет непригоден для будущих сообщений. Мы вернемся к этому чуть позже.
Как оказалось, удаление слагов представляет интересную проблему: как мы узнаем, какие слагы ссылаются на конкретный идентификатор сообщения? Как мы уже видели, мы можем легко найти идентификатор поста с указанным слагом, но у нас нет простого способа найти всех слагов, которые ссылаются на заданный идентификатор поста. В нашей текущей реализации мы должны были бы искать каждого слага в хэше и выбирать только те, которые соответствуют данному идентификатору поста. Это будет линейная операция по общему количеству слагов, и это слишком медленно. Эта проблема также является хорошим предлогом для представления «установленного» типа данных Redis.
Изменение — это всего лишь простая бухгалтерия. У каждого идентификатора поста будет связанный набор, членами которого являются слагы, которые соответствуют этому идентификатору поста. То есть всякий раз, когда мы сопоставляем слаг с идентификатором поста, мы также добавляем этот слаг к набору слагов, связанных с этим идентификатором поста. Более того, поскольку можно обновить слаг для указания другого идентификатора поста, нам сначала нужно удалить этот слаг из набора слагов для старого идентификатора поста.
Вот изменение:
class Slug | |
class << self | |
def [](slug) | |
redis.hget(hash, slug) | |
end | |
def []=(slug, id) | |
if old = self[slug] | |
redis.srem(set(old), slug) | |
end | |
redis.hset(hash, slug, id) | |
redis.sadd(set(id), slug) | |
end | |
private | |
def redis | |
$redis | |
end | |
def hash | |
«post_ids» | |
end | |
def set(id) | |
«post_slugs_#{id}« | |
end | |
end | |
end |
Теперь, когда у нас есть эффективный способ извлечения всех слагов для данного идентификатора поста, мы готовы реализовать метод destroy
:
class Slug | |
class << self | |
def [](slug) | |
redis.hget(hash, slug) | |
end | |
def []=(slug, id) | |
if old = self[slug] | |
redis.srem(set(old), slug) | |
end | |
redis.hset(hash, slug, id) | |
redis.sadd(set(id), slug) | |
end | |
def destroy(id) | |
redis.smembers(set(id)).each { |slug| redis.hdel(hash, slug) } | |
redis.del(set(id)) | |
end | |
private | |
def redis | |
$redis | |
end | |
def hash | |
«post_ids» | |
end | |
def set(id) | |
«post_slugs_#{id}« | |
end | |
end | |
end |
Мы уничтожим отображение из PostObserver
:
class PostObserver | |
def after_save(post) | |
Slug[post.slug] = post.id | |
return true | |
end | |
def after_destroy(post) | |
Slug.destroy(post.id) | |
return true | |
end | |
end |
уникальность
Как упомянуто выше, другой важный аспект нашей реализации slug — убедиться, что в сообщении никогда не будет slug, сопоставленного с другим. В нашей модели Post
мы должны реализовать валидацию, которая гарантирует наличие слага и что он либо не указывает на идентификатор записи, либо указывает на то же сообщение, которое мы в данный момент сохраняем (в случае обновления):
# app/models/post.rb | |
class Post < ActiveRecord::Base | |
validate :ensure_slug_uniqueness | |
protected | |
# validate | |
def ensure_slug_uniqueness | |
# we also want to ensure the slug is not blank | |
if self.slug.blank? | |
errors.add(:slug, «can’t be blank») | |
end | |
# if this is a new post, the id is nil | |
# otherwise, the slug should point to this post’s id | |
unless Slug[self.slug].nil? || Slug[self.slug] == self.id.to_s | |
errors.add(:slug, «is already taken») | |
end | |
end | |
end |
Обратите внимание, что это означает, что автоматическая генерация слагов должна выполняться перед проверкой.
Сохранить идентификаторы сообщений в качестве слагов
Последняя небольшая часть бухгалтерии связана с отображением идентификаторов почтовых ящиков в Redis. Как упомянуто выше, есть ссылки во всем Интернете в этом формате: http://myrailsblog.com/321. После того, как мы запустили нашу реализацию slug, когда кто-то посещает URL-адрес публикации, мы будем искать «321» в Redis, чтобы увидеть, соответствует ли он идентификатору записи. Если мы не хотим, чтобы все эти старые URL-адреса перенаправлялись на домашнюю страницу, нам нужно запустить простой скрипт:
Post.find_each { |post| Slug[post.id.to_s] = post.id.to_s } |
Конечно, другой вариант заключается в том, чтобы модифицировать PostController
чтобы он также находил сообщения по идентификатору в случае, если Redis не имеет сопоставления, но это немного менее эффективно:
# app/controller/posts_controller.rb | |
class PostsController < ApplicationController | |
before_filter :find_post | |
protected | |
def find_post | |
if id = Slug[params[:id]] | |
@post = Post.find(id) | |
else | |
@post = Post.find(params[:id]) | |
end | |
rescue ActiveRecord::RecordNotFound | |
redirect_to root_url | |
end | |
end |
Все сделано
Благодаря этому мы успешно внедрили слагов для нашей платформы блогов. Даже если будет следовать старый URL-адрес поста, наша платформа все равно найдет правильный пост. Более того, мы сделали все это без необходимости изменять схему базы данных, и мы получаем преимущества от быстрого поиска в Redis.
Я надеюсь, что вы нашли этот пример полезным и продолжаете находить творческие способы использовать Redis самостоятельно.