Redis: Persistence Power

Preview:

DESCRIPTION

A practical, use case approach to looking into Redis.

Citation preview

Redis: Persistence PowerNick Quaranto / @qrush / nick@quaran.to

Tuesday, August 10, 2010

What is Redis?

“advanced key-value store”

REmote DIctionary Server

data structures server

Tuesday, August 10, 2010

YOUR APP

Tuesday, August 10, 2010

YOUR APP

REDIS

Tuesday, August 10, 2010

the basics

persist data as you think of it

in memory, sync to disk in background

ridiculously fast

master-slave replication

keys = strings, value = data structures

Tuesday, August 10, 2010

http://try.redis-db.com

Tuesday, August 10, 2010

use it: redis-cli

% ./redis-cli SET user:1:name qrushOK

% ./redis-cli GET user:1:name "qrush"

Tuesday, August 10, 2010

use it: redis-rb% gem install redis% irb -rubygems -rredis

>> $redis = Redis.new=> #<Redis client v2.0.3 connected...

>> $redis.set "user:1:name", "qrush"=> "OK"

>> $redis.get "user:1:name"=> "qrush"

Tuesday, August 10, 2010

FEATURE SWITCHESSTRINGS

based onhttp://github.com/blog/677http://github.com/bvandenbos/redis_feature_control

Tuesday, August 10, 2010

Rediswitch.features << :super_secretRediswitch.features << :payment_gatewayRediswitch.features << :twitter

if Rediswitch.enabled?(:twitter) # post to twitterelse # failwhale ahoy!end

Tuesday, August 10, 2010

begin # take some moneyrescue PaymentGateway::TotallyDown => ohno Rediswitch.disable(:payment_gateway) # notify the troopsend

Tuesday, August 10, 2010

class Rediswitch def self.enabled?(feature) $redis.exists(feature) end

def self.enable(feature) $redis.incr(feature) end def self.disable(feature) $redis.del(feature) endend

Tuesday, August 10, 2010

feature switch lessons

the real win: no-deploy configuration

fast enough to be transparent

next step: separate users into buckets with sets

http://github.com/jamesgolick/rollout

Tuesday, August 10, 2010

RATE LIMITERSTRINGS

soon to be in place athttp://hoptoadapp.com

Tuesday, August 10, 2010

class Choker def restrict? track count_for > 60 endend

Tuesday, August 10, 2010

class Choker def count_for $memcache.get(key, true).to_i endend

Tuesday, August 10, 2010

class Choker def track if !$memcache.get(key, true) $memcache.add(key, "0", 1.minute.from_now, true) end $memcache.incr(key) endend

Tuesday, August 10, 2010

class Choker def track if !$redis.exists(key) $redis.setex(key, 60, 0) end $redis.incr(key) endend

Tuesday, August 10, 2010

class Choker def count_for $redis.get(key).to_i endend

Tuesday, August 10, 2010

rate limiter lessons

expire semantics are changing in redis 2.2

benchmark the crap out of it

could use a sorted set instead of strings

Tuesday, August 10, 2010

API USAGE LOGGINGSTRINGSSORTED SETS

based offhttp://www.production-hacks.com/2010/07/10/redis-api-access-logger/

Tuesday, August 10, 2010

# one way to do itclass ActionHit < ActiveRecord::Base # t.string :controller # t.string :action # t.integer :counterend

class UserHit < ActiveRecord::Base # t.string :controller_action # t.integer :user_id # t.integer :counterend

Tuesday, August 10, 2010

# for all controllers{ "statuses#update" => 1410, "users#create" => 931, "home#index" => 2936}

# users hitting an action{ "101" => 42, "102" => 13, "103" => 34}

Tuesday, August 10, 2010

class StatusesController < ApplicationController def update $redis.incr "statuses#update" $redis.incr "statuses#update:#{user.id}" endend

Tuesday, August 10, 2010

class StatusesController < ApplicationController def update key = "statuses#update" $redis.zincrby "actions", 1, key $redis.zincrby "users:#{key}", 1, user.id endend

Tuesday, August 10, 2010

# hits for a specific user>> $redis.zscore "users:statuses#update", 1001=> 42 # list all the controller actions, sorted>> $redis.zrevrange "actions", 0, -1, :with_scores => true

=> ["home#index", "2936", "statuses#update", "1410", "users#create", "931"]

Tuesday, August 10, 2010

api usage logging lessons

sorted set = high score list

bad at historical usage, trends

good for a simple heartbeat or pulse

Tuesday, August 10, 2010

JOB QUEUELISTS

based onhttp://github.com/defunkt/resque

Tuesday, August 10, 2010

class Staple @queue = :default

def self.perform(post_id, tempfile) # complex image resizing, cropping endend

Tuesday, August 10, 2010

class Post < ActiveRecord::Base after_save :process_with_stapler

def process_with_stapler Resque.enqueue(Staple, self.id, @tempfile) endend

Tuesday, August 10, 2010

module Resque extend self

def push(queue, item) redis.rpush "q:#{queue}", encode(item) end

def pop(queue) decode redis.lpop("q:#{queue}") endend

Tuesday, August 10, 2010

class Resque::Worker def work loop do if job = Resque.pop(queue) job.perform else sleep 5 end end endend

Tuesday, August 10, 2010

module Resque extend self def bpop(queue) decode redis.blpop("q:#{queue}") endend

Tuesday, August 10, 2010

class Resque::Worker def work loop do job = Resque.bpop(queue) job.perform end endend

Tuesday, August 10, 2010

job queue lessons

guaranteed atomic actions, no row locking

blocking commands simplify daemons

many more queue commands in redis itself!

Tuesday, August 10, 2010

GLOBAL ERRORSSETSMULTI/EXEC

a new feature athttp://hoptoadapp.com

Tuesday, August 10, 2010

# text :globals, :default => '', :null => false

class Project < ActiveRecord::Base def has_global?(name) @globals ||= globals.gsub(/,/,' ').split @globals.include?(name) endend

Tuesday, August 10, 2010

# MORE TABLES!!!!

class Global < ActiveRecord::Base belongs_to :projectend

class Project < ActiveRecord::Base has_many :globalsend

Tuesday, August 10, 2010

# Project#global_errors

["MySQL::Error", "MemCache::Error", "Net::HTTPFatalError"]

Tuesday, August 10, 2010

class Project < ActiveRecord::Base def global_key "project-#{id}-globals" end def has_global?(name) $redis.sismember(global_key, name) end end

Tuesday, August 10, 2010

class Project < ActiveRecord::Base after_save :save_globals

def save_globals $redis.del global_key @globals.each do |g| $redis.sadd global_key, g end endend

Tuesday, August 10, 2010

[Mysql::Error, MemCache::Error, Net::HTTPFatalError]

SISMEMBER SISMEMBER

Tuesday, August 10, 2010

[]

DEL

Tuesday, August 10, 2010

[Mysql::Error]

DEL

SADD

Tuesday, August 10, 2010

[Mysql::Error, OpenURI::HTTPError]

DEL

SADD

SADD

Tuesday, August 10, 2010

[Mysql::Error, OpenURI::HTTPError]

DEL

SADD

SADD

SISMEMBER

[]

Tuesday, August 10, 2010

[Mysql::Error, OpenURI::HTTPError]

MULTI

EXEC

DEL

SADD

SADDSISMEMBER

Tuesday, August 10, 2010

class Project < ActiveRecord::Base after_save :save_globals

def save_globals $redis.multi do $redis.del global_key @globals.each do |g| $redis.sadd global_key, g end end endend

Tuesday, August 10, 2010

global error lessons

avoid joins for simple data

consider race conditions

use append-only file (AOF)

Tuesday, August 10, 2010

MULTIPLAYER NOTEPADPUB/SUB

based onhttp://github.com/laktek/realie

Tuesday, August 10, 2010

# usage: ruby pub.rb room username

data = {"user" => ARGV[1]}loop do msg = STDIN.gets $redis.publish ARGV[0], data.merge('msg' => msg.strip).to_jsonend

Tuesday, August 10, 2010

# sub.rb

$redis = Redis.new(:timeout => 0)$redis.subscribe('rubyonrails', 'rubymidwest') do |on| on.message do |room, msg| data = JSON.parse(msg) puts "##{room} - [#{data['user']}]: #{data['msg']}" endend

Tuesday, August 10, 2010

% ruby pub.rb rubymidwest qrushi give up, i hate markdown

% ruby sub.rb#rubymidwest - [qrush]: i give up, i hate markdown#rubyonrails - [railsn00b]: undefined method posts_path? wtf?#rubymidwest - [turbage]: seriously.

Tuesday, August 10, 2010

multiplayer notepad lessons

combine with other data structures

can subscribe to channels via patterns

concurrency in ruby is hard

use eventmachine! (or node.js)

Tuesday, August 10, 2010

more to learn

know your data! (via @antirez)

command reference on the wiki

active IRC, mailing list

Tuesday, August 10, 2010

AKASENTAI.comredis in the cloud

Tuesday, August 10, 2010

Thanks!http://redis.io @qrushhttp://rediscookbook.com http://scr.bi/redispower

Tuesday, August 10, 2010

BONUS ROUND!I prepared way too many examples. Jackpot!

Tuesday, August 10, 2010

URL SHORTENERSTRINGS

based onhttp://github.com/mattmatt/relink

Tuesday, August 10, 2010

require 'sinatra'require 'redis_url'

post '/' do RedisUrl.new(params[:url]).saveend

Tuesday, August 10, 2010

class RedisUrl attr_accessor :url, :id def initialize(url) @url = url @id = seed # unique string algorithm end

def save $redis.set("relink.url|#{@id}", @url) $redis.set("relink.url.rev|#{@url}", @id) endend

Tuesday, August 10, 2010

get %r{/(.+)} do |url| u = RedisUrl.find(url) if u u.clicked redirect u.url else status 404 endend

Tuesday, August 10, 2010

class RedisUrl def self.find(id) u = $redis.get("relink.url|#{id}") if u redis_url = RedisUrl.new(u) redis_url.id = id redis_url end end def clicked $redis.incr("relink.url.clicks|#{@id}") endend

Tuesday, August 10, 2010

url shortener lessons

common pattern: namespacing

incr/decr assumes value is an integer

wrap behavior into ActiveRecord-like objects

next step: store URLs in a list

Tuesday, August 10, 2010

LIVEDEBUGGINGLISTS

based onhttp://github.com/quirkey/redisk

Tuesday, August 10, 2010

def after_save begin # make request to external service rescue Exception => ex logger.error "this shouldn't ever happen!" logger.error ex logger.error ex.backtrace endend

Tuesday, August 10, 2010

# config/initializers/logger.rb

require 'redisk'path = "#{Rails.env}.log"config.logger = Redisk::Logger.new(path)

Tuesday, August 10, 2010

class Redisk::IO def write(string) redis.rpush "#{name}:_list", string end def self.readlines(name) redis.lrange("#{name}:_list", 0, -1) endend

Tuesday, August 10, 2010

live debugging lessons

enables real-time data about your system

dump serialized/marshalled data fast

run the redis instance on a different box

dive deeper: hummingbird

Tuesday, August 10, 2010

COUNTING DOWNLOADSSTRINGSSORTED SETSHASHES

based onhttp://github.com/rubygems/gemcutter

Tuesday, August 10, 2010

# bad idea, dude

class Download < ActiveRecord::Base belongs_to :rubygemend

class Rubygem < ActiveRecord::Base has_many :downloadsend

Tuesday, August 10, 2010

class Download def self.incr(rubygem) $redis.incr("all") $redis.incr(rubygem) $redis.zincrby("today", 1, rubygem) endend

Tuesday, August 10, 2010

class Download def self.rollover(version) $redis.rename "today", "yesterday"

dls = Hash[*$redis.zrange("yesterday", 0, -1, :with_scores => true)] dls.each do |key, score| $redis.hincrby key, Date.today, score Rubygem.find_by_name(key).increment!(:downloads, score) end endend

Tuesday, August 10, 2010

get "/api/v1/downloads/rails.json" do $redis.hgetall("rails").to_jsonend

# returns...{ "2010-07-09" => 1908, "2010-07-10" => 1032, "2010-07-11" => 1091,}

Tuesday, August 10, 2010

counting downloads lessons

hybrid approach does work!

redis is really not for search

test your migration away from SQL

Tuesday, August 10, 2010

Recommended