Redis

more than cache

David Schovanec

12.1.2017 Prague Ruby Meetup

Redis?

  • Author Salvatore Sanfilippo - antirez blog
  • Written in C, minimal memory footprint and fast
  • In-memory data
  • Key-Value storage
  • Basic data types and useful features (commands)
  • Some kind of transactions
  • Scripting with Lua
  • Publish/Subscribe messaging
  • Extendable with modules
  • Persisting via snapshots or append file
  • Out of box distributed solution via redis sentinel

2

Types and commands

  • Types: String, Hash, List, Set, Sorted Set, "Geo"
  • Control with commands GET, SET, HSET, HGET, LPUSH, RPUSH, LLEN, SADD, SMEMBERS, ZADD, ZRANGE, GEOADD, GEODIST, GEORADIUS...
  • "Subtypes": String, Integer, Bitmap, HyperLogLog
  • Many supporting commands EXPIRE, TYPE, MULTI, WATCH, EXISTS, TYPE, SORT, KEYS, SCAN...
  • Each command has "Big O notation" O(log(N)+M)
  • Best friend - redis.io/commands

3.

Crash Course 1

# Value key
SET "some multiworld key" "hello world"
GET "some multiworld key" # => "hello world"

# Hash
HSET some_data name "David Schovanec" 
HSET some_data username "schovi"
HGETALL some_data
# => 1) "name" 2) "David Schovanec" 3) "username" 4) "schovi"

# List
RPUSH supercalifragilisticexpialidocious "value" "another" "and one more"
LLEN supercalifragilisticexpialidocious # => 3

# Set
SADD meetup:speakers "David" "Jan Rezab"
SADD meetup:organisers "David" "Petra" "Honza"
SINTERSTORE meetup:speakers_and_organisers speakers organisers
SMEMBERS meetup:speakers_and_organisers # => 1) "David"

# Sorted Set
ZADD family_members 30 "David" 29 "Hanka" 0.9534246575 "Sára"
ZRANGE family_members 0 1 WITHSCORES
# => 1) "Sára" 2) "0.95342465750000005" 3) "Hanka" 4) "29"

4.

Crash Course 2

INCR user:1232:clicks #=> 1, then 2, etc...
GETSET user:1232:clicks 0 #=> return last value and set new one
# Set value with expiration
SETEX cache:post:1762:html 3600 "<div><h1>Title</h1><p>content...</p></div>"
GETRANGE cache:post:1762:html 0 7 # => "<div><h1"
# Non-blocking delete
UNLINK cache:post:1762:html 

## Permission handling with BIT operations
SETBIT access:compay:123:editor "\x10\x00\xf0"
SETBIT access:compay:123:graphic "\x82\x00\x00"
BITOP OR access:user:1001 access:compay:123:editor access:compay:123:graphic 
GET access:user:1001
# => "\x92\x00\xf0"

## GEO
GEOADD places 14.3255421 50.0598058 "Praha" 16.5079212 49.2021611 "Brno"
GEODIST places "Praha" "Brno" km # => 183.8881
GEOADD places 15.5105797 49.4045045 "Jihlava"
GEORADIUSBYMEMBER places "Jihlava" 200 km WITHDIST
# =>
1) 1) "Praha" 2) "112.1166"
2) 1) "Jihlava" 2) "0.0000"
3) 1) "Brno" 2) "75.7526"

5.

Modeling data structures

# Create meetup
HMSET meetup:13 id 13 name "Prague Ruby Meetup" attendees 20
SADD meetups 13

# Create user
HMSET user:1001 id 1001 name "David Schovanec" username "Schovi" age 30
SADD users 1001

# User attends meetup
SADD meetup:1:attendees 1001

# User organise meetup
SADD meetup:1:organisers 1001

# (Pseudo language. In redis-cli you cant assign variable)
attendees_keys = SMEMBERS meetup:13:attendees

for(key in attendees_keys) {
  HGETALL key
  # => 1) id 2) 1001 3) name 4) "David Schovanec" 5) username 6)"Schovi" 7) age 8) 30
}
  • Because you can doesn't mean you should.
  • NOT database, more like temporary processing storage

6.

Queues

## Basic Queue
RPUSH queue "Job to be done"
RPUSH queue "Job to be done later"
LPOP queue # => "Job to be done"

## Priority queue
RPUSH queue:high "Urgent job"
RPUSH queue:low "Boring job"
BLPOP queue:high queue:low 2 # => "Urgent job"
BLPOP queue:high queue:low 2 # => "Boring job"
# Next same call waits 2 secs if some other client push element to one of lists

## Reliable queue
LPUSH queue:pending "Job to be done"
RPOPLPUSH queue:pending queue:processing # => "Job to be done"

## Unique jobs with order
SET job:post:768:process_at 1484080658
SET job:post:6757:process_at 1484080687
SET job:post:768:data "#Markdown **bold**"
SET job:post:6757:data "##Title 2"
SADD unique:pending post:768 post:6757
SORT unique:pending BY "job:*:process_at" ASC GET "#" GET "job:*:data" LIMIT 0 1
# => 1) "post:768" 2) "#Markdown **bold**"
# Can be reliable too
SORT unique:pending BY ... STORE some_key

7.

Transactions

## Basic transactions
MULTI
SREM queue job:786 # => QUEUED
SREM queue:processing job:786 # => QUEUED
DEL job:786 # => QUEUED
# Execute transaction. Calls queued commands
EXEC 
# => 1) 1 2) 1 3) 1

## Safe transaction
WATCH queue
SREM queue job:786 # => QUEUED
DEL job:786 # => QUEUED

## Meanwhile other client run
SADD queue job:797

## Back in our client
EXEC 
# => (nil)
# Which means it failed because watched key changed
  • Forget about SQL transactions
  • These are more dumb and simple

8.

Pub/Sub

  • Simple publish/subscribe async messaging
## Client 1
SUBSCRIBE my_channel
# Reading messages... (press Ctrl-C to quit)
# 1) "subscribe" 2) "my_channel" 3) (integer) 1

## Client 2 send message
PUBLISH my_channel "Hello world" # => 1 (count of subscribers. 0 for none)

## Client 1 receive
1) "message"
2) "my_channel"
3) "Hello world"

## Pattern match subscribing
PSUBSCRIBE socket:chat:* socket:news_feed:*
1) "psubscribe" 2) "socket:chat:*" 3) (integer) 1 1) "psubscribe" 2) "socket:news_feed:*" 3) (integer) 2
# PUBLISH socket:chat:1 "Good afternoon" # => 1
1) "pmessage"
2) "socket:chat:*"
3) "socket:chat:1"
4) "Good afternoon"

9.

Lua scripting

  • Lua is scripting language (similar to Javascript)
  • Simple, Fast, Easy to embed in other software
  • Lightweight api to redis
    • redis.call("COMMAND", ...args)
    • redis.pcall(...) which ignore errors
  • Lua modules - json, msgpack, math, struct ...
  • Is called atomically and can be used for:
    • Adding missing commands (pop on sorted set)
    • Group complex functionality
  • Few examples in Reddit thread
  • Tutorial on Redis green

10.

Lua scripting

# Ruby
# Custom ZLPOPZADD with MULTI command
redis.multi do
  job, score = redis.zrange("queue:pending", 0, 1, "WITHSCORES")
  redis.zadd("queue:processing", score, job)
  redis.zrem("queue:pending", job)
end
# Wont fail, but wont work too. Commands are queued and run on end. 
# You dont have access to semi results (it will contains some random values)
# Ruby
# Custom ZLPOPZADD with sequence of commands
job, score = redis.zrange("queue:pending", 0, 1, "WITHSCORES")
redis.zadd("queue:processing", score, job)
redis.zrem("queue:pending", job)
# Will work, but anybody can change data in middle 
# of commands and you wont be able to debug what happened
  • Implementing of custom command ZLPOPZADD
  • Learning through failing

11.

Lua scripting

-- Initialize arguments. There are 2 kinds. KEYS and ARGV
local fromKey = KEYS[1]
local toKey = KEYS[2]
local toMove = redis.call('ZRANGE', fromKey, 0, 0, 'WITHSCORES')
-- If length of result is non empty, then continue
if #toMove > 0 then
  local member = toMove[1]
  local score = toMove[2]
  redis.call('ZREM', fromKey, member)
  redis.call('ZADD', toKey, score, member)
  return member
end
EVAL script_string 2 queue:pending queue:processing # => nil | poped value
# Dont send whole script over network again and again
SCRIPT LOAD script_string # => Script sha1
EVALSHA script_sha1 2 queue:pending queue:processing "argument if needed"

# Debugging in terminal
redis-cli --ldb --eval ./zlpopzadd.lua pending processing , "other arg"

# Enable debugging directly in redis
SCRIPT DEBUG YES|SYNC|NO

12.

  • Calling and debugging

Modules

  • Extending redis functionality in native language
  • Languages C, Rust and other (.so file)
  • Better support in Redis 4
  • Loaded with redis-server start
  • Many modules
  • List of modules - Module Hub

13.

Persistence

Snapshotting

  • Conditional dump to file (SAVE or BGSAVE)
# redis.conf
dbfilename dump.rdb
save 900 1     # save after 15 minutes if 1 key changed
save 300 10    # save after 5 minuts if 10 keys changed
save 60 10000  # save after 1 minute if 10000 keys changed

Appending

  • Save log of changes (BGREWRITEAOF)
# redis.conf
appendonly yes
appendfilename appendonly.aof
# don't fsync, just let the OS flush the data when it wants. Faster.
appendfsync no
# fsync after every write to the append only log . Slow, Safest
appendfsync always
# fsync only one time every second. Compromise.
appendfsync everysec

14.

Speed

  • On average computer redis handle upto 40,000 cps
  • Impact on performance
    • Language and Library used
    • Size of data sending
    • Watch your big O!
  • Bottleneck is communication
    • With pipelining up to 700,000 cps
    • Use script for complex function calls
  • Out of box redis-benchmark
    • redis-benchmark -t get,set -P 20 -n 1000000 -d 100
  • Custom benchmarking like in this blogpost

15.

Thank you

Question time with rewards!

Redis

By schovi

Redis

  • 1,233