IMPORTANT:
Some of the content here is a personal summary/abbreviation of contents on the Offical Redis Guide. Feel free to refer to the official site if you think some of the sections written here are not clear.
Redis Intro
Redis is an in-memory remote database that offers high performance, replication, and a unique data model to produce a platform for solving problems. By supporting five different types of data structures (Strings
, Lists
, Sets
, Hashes
, and Sorted sets
), Redis accommodates a wide variety of problems that can be naturally mapped into what Redis offers.
To be specific, Redis is a very fast non-relational database that stores a mapping of keys to five different types of values. Redis supports in-memory persistent storage on disk, replication to scale read performance, and client-side sharding1 to scale write performance.
- non-relational
- In Redis, there are no tables, and there’s no database-defined or -enforced way of relating data in Redis with other data in Redis. Generally speaking, many Redis users will choose to store data in Redis only when the performance or functionality of Redis is necessary, using other relational or non-relational data storage for data where slower performance is acceptable, or where data is too large to fit in memory economically.
Data Structures in Redis
Structure type | What it contains | Structure read/write ability |
---|---|---|
STRING |
Strings, integers, or floating-point values | Operate on the whole string, parts, increment/ decrement the integers and floats |
LIST |
Linked list of strings | Push or pop items from both ends, trim based on offsets, read individual or multiple items, find or remove items by value |
SET |
Unordered collection of unique strings | Add, fetch, or remove individual items, check membership, intersect, union, difference, fetch random items |
HASH |
Unordered hash table of keys to values | Add, fetch, or remove individual items, fetch the whole hash |
ZSET (sorted set) |
Ordered mapping of string members to floating-point scores, ordered by score | Add, fetch, or remove individual values, fetch items based on score ranges or member value |
In the following sections, we will use the redis cli
to pratice using redis commands and data structures.
String
s in Redis - Basics
The operations available to STRING
s start with what’s available in other key-value
stores. We can GET
values, SET
values, and DEL
values. After you have installed and tested Redis, within redis-cli
you can try to SET
, GET
, and DEL
values in Redis. The basic meanings are shown below in the table:
Command | What it does |
---|---|
GET |
Fetches the value stored at the given key |
SET |
Sets the value stored at the given key |
DEL |
Deletes the key-value pair stored at the given key (works for all types) |
Example demonstrating each of the commands:
1 | 127.0.0.1:6379> set hello value |
Note:
- You see outputs such as
(integer) 1
, which usually represents execution success/true.(nil)
represents execution failure, and(integer) 0
also means failure but to the extent where the output meansfalse
, for example when a value (rather than the key) DNE.
List
s in Redis - Basics
The operations that can be performed on LIST
s are typical of what we find in LinkedList
data structures in almost any programming language. We can push
items to the front and the back of the LIST
with LPUSH
/RPUSH
; we can pop
items from the front and back of the list with LPOP
/RPOP
; we can fetch
an item at a given position with LINDEX
; and we can fetch
a range of items with LRANGE
.
Command | What it does |
---|---|
RPUSH |
Pushes the value onto the right end of the list. If list DNE, creates a new list |
LRANGE |
Fetches a range of values from the list |
LINDEX |
Fetches an item at a given position in the list (begins at index 0 ) |
LPOP |
Pops the value from the left end of the list and returns it |
For example:
1 | 127.0.0.1:6379> rpush test-list item1 |
Note:
- See that duplicates in values are allowed.
Set
s in Redis - Basics
In Redis, SET
s are similar to LIST
s in that they’re a sequence of strings, but unlike LIST
s, Redis SET
s use a hash table to keep all values/strings unique.
Because Redis SET
s are unordered, we can’t push
and pop
items from the ends like we did with LIST
s. Instead, we add
and remove
items by value with the SADD
and SREM
commands. We can also find out whether an item is in the SET
quickly with SISMEMBER
, or fetch
the entire set with SMEMBERS
(this can be slow for large SETs, so be careful)
Comamnd | What it does |
---|---|
SADD |
Adds the item to the set |
SMEMBERS |
Returns the entire set of items |
SISMEMBER |
Checks if an item is in the set |
SREM |
Removes the item from the set, if it exists |
For example:
1 | 127.0.0.1:6379> sadd test-set item1 |
Hash
es in Redis - Basics
Whereas LIST
s and SET
s in Redis hold sequences of items, Redis HASH
es store a mapping of (distinct) keys to values. The values that can be stored in HASH
es are the same as what can be stored as normal STRING
s: strings themselves, or if a value can be interpreted as a number, that value can be incremented or decremented.
In a lot of ways, we can think of HASH
es in Redis as miniature versions of Redis itself.
Command | What it does |
---|---|
HSET |
Creats a hashtable, and stores the key-value pair in tha hashtable |
HGET |
Fetches the value at the given hash key |
HGETALL |
Fetches the entire hashtable |
HDEL |
Removes a key-value pair from the hash, if it exists |
For example:
1 | 127.0.0.1:6379> hset test-hasht key1 value1 |
Sorted set
s/Zset
s in Redis - Basics
Like Redis HASH
es, ZSET
s also hold pairs of key and value. The keys (called member
s) are unique, and the values (called score
s) are limited to floating-point numbers. ZSET
s have the unique property in Redis of being able to be accessed by member (like a HASH), but items can also be accessed by the sorted order and values of the score
s.
Command | What it does |
---|---|
ZADD |
Adds member/key with the given score to the ZSET. The order of input is score and then member . |
ZRANGE |
Fetches the items in the ZSET from their positions in sorted order |
ZRANGEBYSCORE |
Fetches items in the ZSET based on a range of scores |
ZREM |
Removes the item from the ZSET, if it exists |
For example:
1 | 127.0.0.1:6379> zadd test-zset 100 member1 |
Connecting a Python Standalone Container to Redis
The basic idea is as follows (assuming you already have a redis
container):
- create a
bridge
that allows the two containers to automatically resolve each other’s IP by container name - connect your
redis
container to thatbridge
- create a
python
container, connecting it to the samebridge
you just created, and configuring your mount volume(s) - install Redis libraries in the
python
container - connect your to the
redis
container byconnected = redis.Redis(host='<your-redis-container-name>', port=6379)
.
(alternatively, you could usedocker-compose
to achieve the same thing.)
Create a
user-defined bridge
with the nameredis-py
in Docker with the following command:1
$ docker network create -d bridge redis-py
Connect your already exitsed
redis
container to that bridge. If you do not have aredis
container yet, you need to set one up first. You connect theredis
container (calledtest-redis
in this example) to that bridgeredis-py
by running:1
$ docker network connect redis-py test-redis
Note:
- Here you would also need to remember the port name your
redis
container uses. By default, it should be6379
. You can see the port once you run yourredis
container withdocker exec -it test-redis redis-cli
, and it will show, for example:127.0.0.1:6379>
.
- Here you would also need to remember the port name your
Now you need to build another
python
container. You can first pull the image withdocker pull python
, and then start a container with:1
2
3
4
5
6
7$ docker run -it --name=test-py-red \
--network redis-py \
-e REDIS_HOST=test-redis \
-e REDIS_PORT=6379 \
-p 8080:80 \
-v python-vol:/data \
python bashNote that some of the commands above are optional:
-e REDIS_HOST=test-redis
- this will configure your enviromental variable
REDIS_HOST
to be set totest-redis
(yourredis
container name). However, this is not necessary.
- this will configure your enviromental variable
e REDIS_PORT=6379
- this will configure your enviromental variable
REDIS_PORT
to be set to6379
(yourredis
port). However, this is not necessary.
- this will configure your enviromental variable
v python-vol:/data
(recommended to have a mount)- this will mount the directory
/data
in yourpython
container to the external local volumepython-vol
- this will mount the directory
Now, since we specified
python bash
in the last line, it will start in bash inside the container. This allows you to install theredis
library for python:1
$ pip install --trusted-host pypi.python.org Flask Redis
Note:
- A problem with this is that if you remove the container, you will need to execute to re-install this library again. A solution could be to build your own Dockerfile and create your own image with:
1
2
3
4
5FROM python:latest
WORKDIR /app
ADD . /app
RUN pip install --trusted-host pypi.python.org Flask Redis
EXPOSE 80
1
- A problem with this is that if you remove the container, you will need to execute to re-install this library again. A solution could be to build your own Dockerfile and create your own image with:
Finally, you can connect to your
redis
container namestest-redis
with:1
$ python
which enters the
python
command line.1
>>> import redis
which imports the
redis
library you just installed1
>>> connected = redis.Redis(host='test-redis',port=6379)
which connects to the
test-redis
container at port6379
, so that now you can accessredis
contents and comamnds such as:1
>>> connected.set('hello','world')
Antonomy of a Redis Web Application
Consider the case that you are managing a large online retail store, and you have millions of customers using it. Now, besides managing your website’s functionality, you would need several things to consider related to user-generated information:
- login management
- user browsing data
- …
You may thing that you could simply use a database like SQL. However, the problem is that most relational databases are limited to inserting, updating, or deleting roughly 200–2,000 individual rows every second per database server. Though bulk inserts/updates/deletes can be performed faster, a customer will only be updating a small handful of rows for each web page view, so higher-speed bulk insertion doesn’t help here.
At present, due to the relatively large load through the day (assume averaging roughly 1,200 writes per second, close to 6,000 writes per second at peak), you needed to set up 10 relational database servers to deal with the load during peak hours. But now, you would like to try the new redis
tool and see if all those data could be managed more easily.
Login and Cookie Caching
Whenever we sign in to services on the internet, such as bank accounts or web mail, these services remember who we are using cookies. Cookies are small pieces of data that websites ask our web browsers to store and resend on every request to that service.
For login cookies, there are two common methods of storing login information in cookies: a signed cookie or a token cookie.
In general, cookies do not store passwords.
Signed cookies typically store the
username
when they last logged in, or maybe their user ID, and whatever else the service may find useful. Along with this user-specific information, the cookie also includes a signature that allows the server to verify that the information that the browser sent hasn’t been altered (like replacing the login name of one user with another).Token cookies use a series of random bytes as the data in the cookie. The series of random bytes is generated when you logged in successfully, and then the server issues you that long random number token as a secret identifier. Then, on the server, the token is used as a key to look up the user who owns that token by querying a database of some kind. Over time, old tokens can be deleted to make room for new tokens (so that your loggin session expires).
Cookie type | Pros | Cons |
---|---|---|
Signed cookie | Everything needed to verify the cookie is in the cookie. Additional information can be included and signed easily | Correctly handling signatures is hard. It’s easy to forget to sign and/or verify data, allowing security vulnerabilities |
Token cookie | Adding information is easy. Very small cookie, so mobile and slow clients can send requests faster | More information to store on the server. If using a relational database, cookie loading / storing can be expensive |
To get started, we’ll use a HASH
to store our mapping from login cookie tokens to the user that’s logged in.
To check the login, we need to fetch the user based on the token and return it, if it’s available. The following listing shows how we check login cookies.
1
2
3
4# conn=redis.Redis(host='<yourhost>', port='<yourPort>')
def check_token(conn, token):
return conn.hget('login:', token) # Fetch and return the given user, if available.
Then, for a visit, we’ll update the login
HASH
for the user and record the current timestamp for the token in theZSET
of recent users. If the user was viewing an item, we also add the item to the user’s recently viewedZSET
and trim thatZSET
if it grows past 25 items. The function that does all of this can be seen next.1
2
3
4
5
6
7
8
9def update_token(conn, token, user, item=None):
timestamp = time.time() #Get the timestamp.
conn.hset('login:', token, user) #Keep a mapping from the token to the logged-in user.
conn.zadd('recent:', token, timestamp) #Record when the token was last seen.
if item:
conn.zadd('viewed:' + token, item, timestamp) #Record that the user viewed the item.
conn.zremrangebyrank('viewed:' + token, 0, -26) #Remove old items, keeping the most recent 25.And you know what? That’s it. We’ve now recorded when a user with the given session last viewed an item and what item that user most recently looked at. On a server made in the last few years, you can record this information for at least 20,000 item views every second, which is more than three times what we needed to perform against the database. This can be made even faster, which we’ll talk about later. But even for this version, we’ve improved performance by 10–100 times over a typical relational database in this context.
Over time, memory use will grow, and we’ll want to clean out old data. As a way of limiting our data, we’ll only keep the most recent 10 million sessions.
For our cleanup, we’ll
fetch
the size of theZSET
in a loop. If theZSET
is too large, we’ll fetch the oldest items up to 100 at a time (because we’re using timestamps, this is just the first 100 items in the ZSET), remove them from therecent ZSET
, delete the login tokens from thelogin HASH
, and delete the relevantviewed ZSET
s. If the ZSET isn’t too large, we’ll sleep for one second and try again later. The code for cleaning out old sessions is shown next.1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19QUIT = False
LIMIT = 10000000
def clean_sessions(conn):
while not QUIT:
size = conn.zcard('recent:') # Find out how many tokens are known.
if size <= LIMIT:
time.sleep(1) # We’re still under our limit; sleep and try again.
else:
end_index = min(size - LIMIT, 100)
tokens = conn.zrange('recent:', 0, end_index-1) # Fetch the token IDs that should be removed.
session_keys = []
for token in tokens:
session_keys.append('viewed:' + token) # Prepare the key names for the tokens to delete.
conn.delete(*session_keys)
conn.hdel('login:', *tokens)
conn.zrem('recent:', *tokens)
Note:
- The
cleanup
function written above may be run as a daemon process, to be run periodically via a cron job, or even to be run during every execution. As a general rule, if the function includes awhile not QUIT
: line, it’s supposed to be run as a daemon, though it could probably be modified to be run periodically, depending on its purpose.- You also notice that we called three functions with a syntax similar to
conn.delete(*vtokens)
. Basically, we’re passing a sequence of arguments to the underlying function without previously unpacking the arguments. For further details on the semantics of how this works, you can visit the Python language tutorial website by visiting this short url: https://mng.bz/8I7W.
Storing Shopping Carts Info
Because we’ve had such good luck with session cookies and recently viewed items, we’ll push our shopping cart information into Redis. Since we’re already keeping user session cookies in Redis (along with recently viewed items), we can use the same cookie ID for referencing the shopping cart.
The shopping cart that we’ll use is simple: it’s a HASH
that maps an item ID to the quantity of that item that the customer would like to purchase. We’ll have the web application handle validation for item count, so we only need to update counts in the cart as they change. If the user wants more than 0 items, we add the item(s) to the HASH
(replacing an earlier count if it existed). If not, we remove the entry from the hash. Our add_to_cart()
function can be seen in this listing.
1 | def add_to_cart(conn, session, item, count): |
Now, we’ll update our session cleanup
function to include deleting old shopping carts as clean_full_sessions()
in the next listing.
1 | def clean_full_sessions(conn): |
We now have both sessions and the shopping cart stored in Redis, which helps to reduce request size, as well as allows the performing of statistical calculations on visitors to our site based on what items they looked at, what items ended up in their shopping carts, and what items they finally purchased.
Webpage Caching
Caching is different from a cookie. While they both act as some kind of storage, cache is actually a part of computer memory, whereas cookie is just stored and used by web browsers.
In genearl, when a processor wants something (e.g. values(a,b) for doing an operation (a+b)), it first search in cache if cache have these values then processor will fetch values form cache (fetching values from cache is really fast), but if cache don’t have these values then it will search in RAM (fetching values from ram is faster than hdd,ssd but slower than cache) if ram don’t have then it will search in hhd or ssd.
In the context of web services, this means that cache in general can deal with larger data pieces than cookies, so one common way of using it in web services is for storing static website contents. Since those static contenst don’t need to be dynamically generated upon every request, we can store most of the information in cache for faster browsing experience.
1 | def cache_request(conn, request): |
Database row caching
For a daily deal with inventory counts being reduced and affecting whether someone can buy the item, the site cannot be entirely cached. However, an easy solution would be caching but also remmeber updating that specific database row. This means that you can choose to update the cached row for that specific item every few seconds, if there are many buyers. But if the data doesn’t change often, or when back-ordered items are acceptable, it may make sense to only update the cache every minute.
This can be done via using two ZSET
s, one called delay:
that tells you how often you should update your database row, and another called schedule:
which tells you when the database row has been last updated in your cache.
1 | def cache_rows(conn): # this will be a daemon process |
Simple Webpage Analytics
In the previous examples, we were able to cache webpage contents. However, we did not limit how much pages should be cached, as we did not specify the can_cache()
function yet. But what would be a sensible way to decide which webpage should be cached and which one shouldn’t?
A simple idea is to only include (for example) 10,000 webpages that has items you are browing the most often. For the other webpages with items that you browse only occaasionally, you do not store them in cache. However, you also need to be able to include possible new trending items, so you cannot just simply discard all items below the 10,000 limit. One solution you could use is to rescale the view counts of the top 10,000 items you viewed to be half has much as they were before.
1 | def rescale_viewed(conn): |
where the line:
conn.zinterstore('viewed:', {'viewed:': .5})
, the functionZINTERSTORE
lets us combine one or moreZSET
s and multiply every score in the inputZSET
s by a given number. (Each inputZSET
can be multiplied by a different number.)
Now, one sensible way to call this function is to place it in the place where you kept a reference to every item visited when you do login and shopping cart info storing. So you can place it in here:
1 | def update_token(conn, token, user, item=None): |
With the rescaling
and the counting
, we now have a constantly updated list of the most-frequently viewed items. Now all we need to do is to specify our can_cache()
function to take into consideration our new method of deciding whether a page can be cached, and we’re done:
1 | def can_cache(conn, request): |
String
s in Redis
In this section, we’ll talk about the simplest structure available to Redis, the STRING. This builds on the section String
s in Redis - Basics, and here it covers the basic numeric increment and decrement operations, followed later by the bit and substring manipulation calls, and you’ll come to understand that even the simplest of structures has a few surprises that can make it useful in a variety of powerful ways.
Command | Example use and description |
---|---|
INCR |
INCR key-name — Increments the value stored at the key by 1 |
DECR |
DECR key-name — Decrements the value stored at the key by 1 |
INCRBY |
INCRBY key-name amount — Increments the value stored at the key by the provided integer value |
DECRBY |
DECRBY key-name amount — Decrements the value stored at the key by the provided integer value |
INCRBYFLOAT |
INCRBYFLOAT key-name amount — Increments the value stored at the key by the provided float value (available in Redis 2.6 and later) |
This means that, when setting a STRING
value in Redis, if that value could be interpreted as a base-10 integer or a floating-point value, Redis will detect this and allow you to manipulate the value using the various INCR*
and DECR*
operations. So, if you try to increment or decrement a key that has a value that can’t be interpreted as an integer or float, you’ll receive an error.
Note:
- If you try to increment or decrement a key that doesn’t exist or is an empty string, Redis will operate as though that key’s value were zero.
1 | import redis; conn=redis.Redis(host='cust-redis',port=6379) |
Note:
- You see those
b
in front of the result string because these data are byte literals instead of String objects. Therefore, to get rid of theb
, you can add the methoddecode('utf8')
to the end.
Redis additionally offers methods for reading and writing parts of byte string values (integer and float values can also be accessed as though they’re byte strings, though that use is somewhat uncommon).
Command | Example use and description |
---|---|
APPEND |
APPEND key-name value — Concatenates the provided value to the string already stored at the given key |
GETRANGE |
GETRANGE key-name start end — Fetches the substring, including all characters from the start offset to the end offset, inclusive |
SETRANGE |
SETRANGE key-name offset value — Sets the substring starting at the provided offset to the given value |
GETBIT |
GETBIT key-name offset — Treats the byte string as a bit string, and returns the value of the bit in the string at the provided bit offset |
SETBIT |
SETBIT key-name offset value — Treats the byte string as a bit string, and sets the value of the bit in the string at the provided bit offset |
BITCOUNT |
BITCOUNT key-name [start end] — Counts the number of 1 bits in the string, optionally starting and finishing at the provided byte offsets |
BITOP |
BITOP operation dest-key key-name [key-name …] — Performs one of the bitwise operations, AND, OR, XOR, or NOT, on the strings provided, storing the result in the destination key |
1 | 'new-string-key', 'hello ') # append the string ‘hello ’ to the previously nonexistent key conn.append( |
List
s in Redis
In this section, we’ll talk about LIST
s, which store an ordered sequence of STRING
values. This builds on from the section List
s in Redis - Basics. We’ll cover some of the most commonly used LIST
manipulation commands for pushing and popping items from LIST
s.
Command | Example use and description |
---|---|
RPUSH |
RPUSH key-name value [value …] — Pushes the value(s) onto the right end of the list |
LPUSH |
LPUSH key-name value [value …] — Pushes the value(s) onto the left end of the list |
RPOP |
RPOP key-name — Removes and returns the rightmost item from the list |
LPOP |
LPOP key-name — Removes and returns the leftmost item from the list |
LINDEX |
LINDEX key-name offset — Returns the item at the given offset/index |
LRANGE |
LRANGE key-name start end — Returns the items in the list at the offsets from start to end, inclusive |
LTRIM |
LTRIM key-name start end — Trims the list to only include items at indices between start and end, inclusive |
1 | 'list-key', 'last') conn.rpush( |
Among the LIST
commands we didn’t introduce before are a few commands that allow you to move items from one list to another, and even block while waiting for other clients to add items to LIST
s.
Command | Example use and description |
---|---|
BLPOP |
BLPOP key-name [key-name …] timeout — Pops the leftmost item from the first non-empty LIST, or waits the timeout in seconds for an item |
BRPOP |
BRPOP key-name [key-name …] timeout — Pops the rightmost item from the first non-empty LIST, or waits the timeout in seconds for an item |
RPOPLPUSH |
RPOPLPUSH source-key dest-key — Pops the rightmost item from the source and LPUSHes the item to the destination, also returning the item to the user |
BRPOPLPUSH |
BRPOPLPUSH source-key dest-key timeout — Pops the rightmost item from the source and LPUSHes the item to the destination, also returning the item to the user, and waiting up to the timeout if the source is empty |
1 | # Let’s add some items to a couple of lists to start. |
Set
s in Redis
In this section, we’ll discuss some of the most frequently used commands that operate on SET
s. This builds on from the section Set
s in Redis - Basics. You’ll learn about the standard operations for inserting, removing, and moving members between SET
s, as well as commands to perform intersection, union, and differences on SET
s.
Command | Example use and description |
---|---|
SADD |
SADD key-name item [item …] — Adds the items to the set and returns the number of items added that weren’t already present |
SREM |
SREM key-name item [item …] — Removes the items and returns the number of items that were removed |
SISMEMBER |
SISMEMBER key-name item — Returns whether the item is in the SET |
SCARD |
SCARD key-name — Returns the number of items in the SET |
SMEMBERS |
SMEMBERS key-name — Returns all of the items in the SET as a Python set |
SRANDMEMBER |
SRANDMEMBER key-name [count] — Returns one or more random items from the SET. When count is positive, Redis will return count distinct randomly chosen items, and when count is negative, Redis will return count randomly chosen items that may not be distinct. |
SPOP |
SPOP key-name — Removes and returns a random item from the SET |
SMOVE |
SMOVE source-key dest-key item — If the item is in the source, removes the item from the source and adds it to the destination, returning if the item was moved |
1 | 'set-key', 'a', 'b', 'c') conn.sadd( |
Hash
es in Redis
In this section, we’ll talk about the most commonly used commands that manipulate HASH
es. This builds on fro the section Hash
es in Redis - Basics. You’ll learn more about the operations for adding and removing key-value pairs to HASH
es, as well as commands to fetch all of the HASH
contents along with the ability to increment or decrement values.
Command | Example use and description |
---|---|
HMGET |
HMGET key-name key [key …] — Fetches the values at the fields/keys in the HASH |
HMSET |
HMSET key-name key value [key value …] — Sets the values of the fields in the HASH |
HDEL |
HDEL key-name key [key …] — Deletes the key-value pairs in the HASH, returning the number of pairs that were found and deleted |
HLEN |
HLEN key-name — Returns the number of key-value pairs in the HASH |
1 | 'hash-key', {'k1':'v1', 'k2':'v2', 'k3':'v3'}) conn.hmset( |
Note:
- The
HMGET
/HMSET
commands are similar to their single-argument versions (HSET
/HGET
), only differing in that they take a list or dictionary for arguments instead of the single entries.
With the availability of HGETALL
, it may not seem as though HKEYS
and HVALUES
would be that useful, but when you expect your values to be large, you can fetch the keys, and then get the values one by one to keep from blocking other requests.
Command | Example use and description |
---|---|
HEXISTS |
HEXISTS key-name key — Returns whether the given key exists in the HASH |
HKEYS |
HKEYS key-name — Fetches the keys in the HASH |
HVALS |
HVALS key-name — Fetches the values in the HASH |
HGETALL |
HGETALL key-name — Fetches all key-value pairs from the HASH |
HINCRBY |
HINCRBY key-name key increment — Increments the value stored at the given key by the integer increment |
HINCRBYFLOAT |
HINCRBYFLOAT key-name key increment — Increments the value stored at the given key by the float increment |
HINCRBY
and HINCRBYFLOAT
should remind you of the INCRBY
and INCRBYFLOAT
operations available on STRING
keys, and they have the same semantics, applied to HASH
values.
1 | 'hash-key2', {'short':'hello', 'long':1000*'1'}) conn.hmset( |
ZSet
s in Redis
In this section, we’ll talk about commands that operate on ZSET
s. This builds on from the section ZSet
s in Redis - Basics. You’ll learn how to add and update items in ZSET
s, as well as how to use the ZSET
intersection and union commands.
Command | Example use and description |
---|---|
ZADD |
ZADD key-name score member [score member …] — Adds members with the given scores to the ZSET |
ZREM |
ZREM key-name member [member …] — Removes the members from the ZSET, returning the number of members that were removed |
ZCARD |
ZCARD key-name — Returns the number of members in the ZSET |
ZINCRBY |
ZINCRBY key-name increment member — Increments the member in the ZSET |
ZCOUNT |
ZCOUNT key-name min max — Returns the number of members with scores between the provided minimum and maximum |
ZRANK |
ZRANK key-name member — Returns the position of the given member in the ZSET |
ZSCORE |
ZSCORE key-name member — Returns the score of the member in the ZSET |
ZRANGE |
ZRANGE key-name start stop [WITHSCORES] — Returns the members and optionally the scores for the members with ranks between start and stop |
1 | 'zset-key', 'a', 3, 'b', 2, 'c', 1) # Adding members to ZSETs in Python has the arguments conn.zadd( |
For debugging, we usually fetch the entire ZSET
with this ZRANGE
call, but real use cases will usually fetch items a relatively small group at a time.
Below shows several more ZSET
commands in Redis that you’ll find useful.
Command | Example use and description |
---|---|
ZREVRANK |
ZREVRANK key-name member — Returns the position of the member in the ZSET, with members ordered in reverse |
ZREVRANGE |
ZREVRANGE key-name start stop [WITHSCORES] — Fetches the given members from the ZSET by rank, with members in reverse order |
ZRANGEBYSCORE |
ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT offset count] — Fetches the members between min and max |
ZREVRANGEBYSCORE |
ZREVRANGEBYSCORE key max min [WITHSCORES] [LIMIT offset count] — Fetches the members in reverse order between min and max |
ZREMRANGEBYRANK |
ZREMRANGEBYRANK key-name start stop — Removes the items from the ZSET with ranks between start and stop |
ZREMRANGEBYSCORE |
ZREMRANGEBYSCORE key-name min max — Removes the items from the ZSET with scores between min and max |
ZINTERSTORE |
ZINTERSTORE dest-key key-count key [key …] [WEIGHTS weight [weight …]] — Performs a SET-like intersection of the provided ZSETs |
ZUNIONSTORE |
ZUNIONSTORE dest-key key-count key [key …] [WEIGHTS weight [weight …]] — Performs a SET-like union of the provided ZSETs |
1 | # We’ll start out by creating a couple of ZSETs. |
Publish
/Subscribe
in Redis
Generally, the concept of publish
/subscribe
, also known as pub/sub
, is characterized by listeners subscribing to channels
, with publishers sending binary string messages to channels
.
- Anyone listening to a given channel will receive all messages sent to that channel while they’re connected and listening. You can think of it like a radio station, where subscribers can listen to multiple radio stations at the same time
- Any publishers can send messages to a given channel, like being a radio station.
Command | Example use and description |
---|---|
SUBSCRIBE |
SUBSCRIBE channel [channel …] — Subscribes to the given channels |
UNSUBSCRIBE |
UNSUBSCRIBE [channel [channel …]] — Unsubscribes from the provided channels, or unsubscribes all channels if no channel is given |
PUBLISH |
PUBLISH channel message — Publishes a message to the given channel |
PSUBSCRIBE |
PSUBSCRIBE pattern [pattern …] — Subscribes to messages broadcast to channels that match the given pattern |
PUNSUBSCRIBE |
PUNSUBSCRIBE [pattern [pattern …]] — Unsubscribes from the provided patterns, or unsubscribes from all subscribed patterns if none are given |
1 | def publisher(n): |
Note:
- But in the case of clients that have subscribed, if the client is disconnected and a message is sent before it can reconnect, the client will never see the message. When you’re relying on receiving messages over a channel, the semantics of
PUBLISH
/SUBSCRIBE
in Redis may let you down.- However, there are different methods to handle reliable message delivery (you will see in a later section), which works in the face of network disconnections, and which won’t cause Redis memory to grow (even in older versions of Redis) unless you want it to.
Operations in Redis - Sorting
Sorting in Redis is similar to sorting in other languages: we want to take a sequence of items and order them according to some comparison between elements. SORT
allows us to sort LIST
s, SET
s, and ZSET
s according to data in the LIST
/SET
/ZSET
data stored in STRING
keys, or even data stored in HASH
es. If you’re coming from a relational database background, you can think of SORT
as like the order by
clause in a SQL statement that can reference other rows and tables.
Command | Example use and description |
---|---|
SORT |
SORT source-key [BY pattern] [LIMIT offset count] [GET pattern [GET pattern …]] ... — Sorts the input LIST , SET , or ZSET according to the options provided, and returns or stores the result |
Some of the more basic options with SORT
include the ability to order the results in descending order rather than the default ascending order, consider items as though they were numbers, compare as though items were binary strings, and etc.
1 | # Start by adding some items to a LIST. |
Sorting can be used to sort LIST
s, but it can also sort SET
s, turning the result into a LIST
.
Though SORT
is the only command that can manipulate three types of data at the same time, basic Redis transactions can let you manipulate multiple data types with a series of commands without interruption.
Basic Redis Transactions
Though there are a few commands to copy or move items between keys, there isn’t a single command to move items between types (though you can copy from a SET
to a ZSET
with ZUNIONSTORE
). For operations involving multiple keys (of the same or different types), Redis has five commands that help us operate on multiple keys without interruption: WATCH
, MULTI
, EXEC
, UNWATCH
, and DISCARD
.
In Redis, a basic transaction involving MULTI and EXEC is meant to provide the opportunity for one client to execute multiple commands A, B, C, … without other clients being able to interrupt them. This isn’t the same as a relational database transaction, which can be executed partially, and then rolled back or committed. In Redis, every command passed as part of a basic MULTI/EXEC transaction is executed one after another until they’ve completed. After they’ve completed, other clients may execute their commands.
To perform a transaction in Redis, we first call MULTI
, followed by any sequence of commands we intend to execute, followed by EXEC
. When seeing MULTI
, Redis will queue up commands from that same connection until it sees an EXEC
, at which point Redis will execute the queued commands sequentially without interruption.
For example, in a redis-cli
:
1 | 127.0.0.1:6379> multi |
Semantically, Python library
handles this by the use of what’s called a pipeline
. Calling the pipeline()
method on a connection object will create a transaction, which when used correctly will automatically wrap a sequence of commands with MULTI
and EXEC
. Incidentally, the Python Redis client will also store the commands to send until we actually want to send them. This reduces the number of round trips between Redis and the client, which can improve the performance of a sequence of commands.
1 | import redis |
Now, notice that by using a transaction, each thread is able to execute its entire sequence of commands without other threads interrupting it (as otherwise one of them might print 1
instead of 0
), despite the delay between the two calls.
Basically, you can think of this as having the same effect as synchrnoized
keyword in Java, so that this transaction is locked once one thread enters.
Key Expiration
When we say that a key has a time to live, or that it’ll expire at a given time, we mean that Redis will automatically delete the key when its expiration time has arrived.
Having keys that will expire after a certain amount of time can be useful to handle the cleanup of cached data. If you look through other chapters, you won’t see the use of key expiration in Redis often. This is mostly due to the types of structures that are used; few of the commands we use offer the ability to set the expiration time of a key automatically. And with containers (LIST
s, SET
s, HASH
es, and ZSET
s), we can only expire entire keys, not individual items (this is also why we use ZSET
s with timestamps in a few places).
Command | Example use and description |
---|---|
PERSIST |
PERSIST key-name — Removes the expiration from a key |
TTL |
TTL key-name — Returns the amount of time remaining before a key will expire |
EXPIRE |
EXPIRE key-name seconds — Sets the key to expire in the given number of seconds |
EXPIREAT |
EXPIREAT key-name timestamp — Sets the expiration time as the given Unix timestamp |
PTTL |
PTTL key-name — Returns the number of milliseconds before the key will expire (available in Redis 2.6 and later) |
PEXPIRE |
PEXPIRE key-name milliseconds — Sets the key to expire in the given number of milliseconds (available in Redis 2.6 and later) |
PEXPIREAT |
PEXPIREAT key-name timestamp-milliseconds — Sets the expiration time to be the given Unix timestamp specified in milliseconds (available in Redis 2.6 and later) |
1 | # We’re starting with a very simple STRING value. |
Persistence in Redis - Snapshot
In Redis, we can create a point-in-time copy of in-memory data by creating a snapshot. After creation, these snapshots can be backed up, copied to other servers to create a clone of the server, or left for a future restart.
On the configuration side of things, snapshots are written to the file referenced as dbfilename
in the configuration, and stored in the path referenced as dir
. Until the next snapshot is performed, data written to Redis since the last snapshot started (and completed) would be lost if there were a crash caused by Redis, the system, or the hardware.
As an example, say that we have Redis running with 10 gigabytes of data currently in memory. A previous snapshot had been started at 2:35 p.m. and had finished. Now a snapshot is started at 3:06 p.m., and 35 keys are updated before the snapshot completes at 3:08 p.m. If some part of the system were to crash and prevent Redis from completing its snapshot operation between 3:06 p.m. and 3:08 p.m., any data written between 2:35 p.m. and now would be lost. But if the system were to crash just after the snapshot had completed, then only the updates to those 35 keys would be lost (so that the snapshot only includes data at the point of initiation, at 3:06 p.m.).
There are five methods to initiate a snapshot, which are listed as follows:
- Any Redis client can initiate a snapshot by calling the
BGSAVE
command. On platforms that supportBGSAVE
(basically all platforms except for Windows), Redis will fork, 1 and the child process will write the snapshot to disk while the parent process continues to respond to commands.- When a process forks, the underlying operating system makes a copy of the process. On Unix and Unix-like systems, the copying process is optimized such that, initially, all memory is shared between the child and parent processes. When either the parent or child process writes to memory, that memory will stop being shared.
- A Redis client can also initiate a snapshot by calling the
SAVE
command, which causes Redis to stop responding to any/all commands until the snapshot completes. This command isn’t commonly used, except in situations where we need our data on disk, and either we’re okay waiting for it to complete, or we don’t have enough memory for a BGSAVE. - If Redis is configured with
save
lines, such assave 60 10000
, Redis will automatically trigger aBGSAVE
operation if 10,000 writes have occurred after 60 seconds since the last successful save has started (using the configuration option described). When multiplesave
lines are present, any time one of the rules match, aBGSAVE
is triggered. - When Redis receives a request to shut down by the
SHUTDOWN
command, or it receives a standardTERM
signal, Redis will perform aSAVE
, blocking clients from performing any further commands, and then shut down. - If a Redis server connects to another Redis server and issues the
SYNC
command to begin replication, the master Redis server will start aBGSAVE
operation if one isn’t already executing or recently completed.
Note:
- There are two ways to do the above configuration for
redis
. One is to use theconfig set <value>
command. The other is to change theredis.conf
file, which should be under/usr/local/etc/redis/redis.conf
.- For
docker
users, if that file does not exist, you might need to manually create one and mount that directory.
However, as mentioned before, if a system crashes while you are doing a snapshot
, data will be lost. Then the question becomes how to recover it. Suppose you are processing log files, and the system crashes before you are diong a dump. Then, in order to recover from it, you need to find a way to first know what has been lost, in other words, you need to somehow track your progress:
1 | def process_logs(conn, path, callback): # the callback function will be used to process the log line |
Deciding whether to use BGSAVE
or SAVE
When the amount of data that we store in Redis tends to be under a few gigabytes, snapshotting with BGSAVE
can be the right answer. Redis will fork, save to disk, and finish the snapshot faster than you can read this sentence. But as our Redis memory use grows over time, so does the time to perform a fork operation for the BGSAVE
. In situations where Redis is using tens of gigabytes of memory, there isn’t a lot of free memory, or if we’re running on a virtual machine, letting a BGSAVE occur may cause the system to pause for extended periods of time.
To prevent forking from causing such issues, we may want to disable automatic saving entirely. When automatic saving is disabled, we then need to manually call BGSAVE
(which has all of the same potential issues as before, only now we know when they will happen), or we can call SAVE
. With SAVE
, Redis does block until the save is completed, but because there’s no fork, there’s no fork delay. And because Redis doesn’t have to fight with itself for resources, the snapshot will finish faster.
For example, when trying to use BGSAVE
with clients writing to Redis with data of about 50 gigabytes, forking would take 15 seconds or more, followed by 15–20 minutes for the snapshot to complete. But with SAVE
, the snapshot would finish in 3–5 minutes.
Persistence in Redis - Append-only file
In basic terms, append-only log files keep a record of data changes that occur by writing each change to the end of the file. In doing this, anyone could recover the entire dataset by replaying the append-only log from the beginning to the end. Redis has functionality that does this as well, and it’s enabled by setting the configuration option appendonly yes
.
You also have several options to configuring syncing and writing the append-only file to disk. When that sync is completed, we can be fairly certain that our data is on disk and we can read it later if the system otherwise fails.
appendfsync Option |
How often syncing will occur |
---|---|
always |
Every write command to Redis results in a write to disk. This slows Redis down substantially if used. |
everysec |
Once per second, explicitly syncs write commands to disk. |
no |
Lets the operating system control syncing to disk. |
Warning:
You’ll want to be careful if you’re using SSDs with appendfsync always
. Writing every change to disk as they happen, instead of letting the operating system group writes together as is the case with the other appendfsync
options, has the potential to cause an extreme form of what is known as write amplification. By writing small amounts of data to the end of a file, you can reduce the lifetime of SSDs from years to just a few months in some cases.
As a reasonable compromise between keeping data safe and keeping our write performance high, we can also set appendfsync everysec
. This configuration will sync the append-only log once every second. For most common uses, we’ll likely not find significant performance penalties for syncing to disk every second compared to not using any sort of persistence. By syncing to disk every second, if the system were to crash, we could lose at most one second of data that had been written or updated in Redis. Also, in the case where the disk is unable to keep up with the write volume that’s happening, Redis would gracefully slow down to accommodate the maximum write rate of the drive.
Append-only files are flexible, offering a variety of options to ensure that almost every level of paranoia can be addressed. But there’s a dark side to AOF persistence, and that is file size.
Configuring/Compacting your Append-only Files
If using AOF
would make data loss to one second of work (for example, appendfsync everysecond
), then why do we want to use Snapshots
in general? Isn’t the choice clear that we would want to use AOF
almost always? In reality, the choice is actually not so simple: because every write to Redis causes a log of the command to be written to disk, the append-only log file will continuously grow. Over time, a growing AOF could cause your disk to run out of space, but more commonly, upon restart, Redis will be executing every command in the AOF in order. When handling large AOFs, Redis can take a very long time to start up.
One solution is to use BGREWRITEAOF
, which will rewrite the AOF to be as short as possible by removing redundant commands. BGREWRITEAOF
works similarly to the snapshotting BGSAVE
: performing a fork and subsequently rewriting the append-only log in the child. As such, all of the same limitations with snapshotting performance regarding fork time, memory use, and so on still stand when using append-only files. But even worse, because AOF
s can grow to be many times the size of a dump (if left uncontrolled), when the AOF
is rewritten, the OS needs to delete the AOF
, which can cause the system to hang for multiple seconds while it’s deleting an AOF
of tens of gigabytes.
There are also two configuration options that enable and customize the automatic BGREWRITEAOF
execution:
auto-aof-rewrite-percentage
auto-aof-rewrite-min-size
For example, if we use auto-aof-rewritepercentage 100
and auto-aof-rewrite-min-size 64mb
, when AOF
is enabled, Redis will initiate a BGREWRITEAOF when the AOF
is at least 100% larger than it was when Redis last finished rewriting the AOF, and when the AOF
is at least 64 megabytes in size. As a point of configuration, if our AOF
is rewriting too often, we can increase the 100
that represents 100%
to something larger, though it will cause Redis to take longer to start up if it has been a while since a rewrite happened.
Replication
Replication is a method by which other servers receive a continuously updated copy of the data as the main server is being written, so that the replicas can service read queries. In the relational database world, it’s not uncommon for a single master
database to send writes out to multiple slave
s, with the slave
s performing all of the read queries. Redis has adopted this method of replication as a way of helping to scale, and this section will discuss configuring replication in Redis, and how Redis operates during replication.
With a master
/slave
setup, instead of connecting to the master
for reading data, clients will connect to one of the slave
s to read their data (typically choosing them in a random fashion to try to balance the load).
Configuring Master and Slave Server
To configure replication on the
master
side of things, we only need to ensure that the path and filename listed under thedir
anddbfilename
configuration options shown below are to a path and file that are writable by the Redis process.1
2
3
4# inside the redis.conf file
dbfilename dump.rdb # also the default
dir ./Though a variety of options control behavior of the
slave
itself, only one option is really necessary to enable slaving:slaveof
. If we were to setslaveof host port
in our configuration file, the Redis that’s started with that configuration will use the provided host and port as themaster
Redis server it should connect to. If we have an already running system, we can tell a Redis server to stop slaving, or even to slave to a new or different master. To connect to a newmaster
, we can use theSLAVEOF host port
command, or if we want to stop updating data from the master, we can useSLAVEOF no one
.
Master
/Slave
startup process
These are the operations that occur on both the master
and slave
when a slave
connects to a master
.
Step | Master operations | Slave operations |
---|---|---|
1 | (waiting for a command) | (Re-)connects to the master ; issues the SYNC command |
2 | Starts BGSAVE operation; keeps a backlog of all write commands sent after BGSAVE |
Serves old data (if any), or returns errors to commands (depending on configuration) |
3 | Finishes BGSAVE ; starts sending the snapshot to the slave; continues holding a backlog of write commands |
Discards all old data (if any); starts loading the dump as it’s received |
4 | Finishes sending the snapshot to the slave; starts sending the write command backlog to the slave | Finishes parsing the dump; starts responding to commands normally again |
5 | Finishes sending the backlog; starts live streaming of write commands as they happen | Finishes executing backlog of write commands from the master; continues executing commands as they happen |
So, Redis manages to keep up with most loads during replication, except in cases where network bandwidth between the master
and slave
instances isn’t fast enough, or when the master
doesn’t have enough memory to fork and keep a backlog of write commands. Though it isn’t necessary, it’s generally considered to be a good practice to have Redis masters only use about 50–65% of the memory in our system, leaving approximately 30–45% for spare memory during BGSAVE
and command backlogs.
Note:
- Just to make sure that we’re all on the same page (some users forget this the first time they try using
slave
s): when aslave
initially connects to amaster
, any data that had been in memory will be lost, to be replaced by the data coming from themaster
.
But what happens when you have more than one slave
? When another slave
connects to an existing master, sometimes it can reuse an existing dump file (if there is). In general, only one of the two scenario will happen:
When additional slaves connect | Master operation |
---|---|
Before step 3 in table above | All slaves will receive the same dump and same backlogged write commands. |
On or after step 3 in table above | While the master is finishing up the five steps for earlier slaves, a new sequence of steps 1-5 will start for the new slave(s). |
Master
/Slave
Chain
Some developers have found that when they need to replicate to more than a handful of slave
s, some networks are unable to keep up — especially when replication is being performed over the internet or between data centers. Because there’s nothing particularly special about being a master
or a slave
in Redis, slave
s can have their own slave
s, resulting in master
/slave
chaining.
In general, when read load significantly outweighs write load, and when the number of reads pushes well beyond what a single Redis server can handle, it’s common to keep adding slave
s to help deal with the load. As load continues to increase, we can run into situations where the single master
can’t write to all of its slave
s fast enough, or is overloaded with slave
s reconnecting and resyncing. To alleviate such issues, we may want to set up a layer of intermediate Redis master
/slave
nodes:
Verifying Disk Writes
Verifying that the data we wrote to the master
made it to the slave
is easy: we merely need to write a unique dummy value after our important data, and then check for it on the slave. But verifying that the data made it to disk is more difficult. If we wait at least one second, we know that our data made it to disk. But if we’re careful, we may be able to wait less time by checking the output of INFO
for the value of aof_pending_bio_fsync
, which will be 0
if all data that the server knows about has been written to disk.
Now, if we know this, we could write a program to automate the check for us:
1 | def wait_for_sync(mconn, sconn): |
Here we see the use of the INFO
command. In general, the INFO
command can offer a wide range of information about the current status of a Redis server—memory used, the number of connected clients, the number of keys in each database, the number of commands executed since the last snapshot, and more. Generally speaking, INFO
is a good source of information about the general state of our Redis servers, and many resources online can explain more.
Fixing Snapshot
s and AOF
s
We have covered how to replicate to handle redis
data storage. However, we have not yet covered how to fix the Dump
s or the AOF
s themselves if there is a system failure.
When confronted with system failures, we have tools to help us recover when either snapshotting
or append-only file
logging had been enabled. Redis includes two command-line applications for testing the status of a snapshot
and an append-only file
. These commands are redis-check-aof
and redis-check-dump
. If we run either command without arguments (not in the redis-cli
, but in command-line), we’ll see the basic help that’s provided:
1 | $ redis-check-aof |
If we provide
–fix
as an argument toredis-check-aof
, the command will fix the file. Its method to fix anappend-only file
is simple: it scans through the providedAOF
, looking for an incomplete or incorrect command. Upon finding the first bad command, it trims the file to just before that command would’ve been executed. For most situations, this will discard the last partial write command. However, it is possible that all the correct commands after that first bad command will be lost due to this process.Unfortunately, there’s no currently supported method of repairing a corrupted
snapshot
. Though there’s the potential to discover where the first error had occurred, because the snapshot itself is compressed, an error partway through the dump has the potential to make the remaining parts of the snapshot unreadable. It’s for these reasons that I’d generally recommend keeping multiple backups of important snapshots, and calculating theSHA1
orSHA256
hashes to verify content during restoration.
Now, after we’ve verified that our backups are what we had saved before, and we’ve corrected the last write to AOF
as necessary, the last step is for us to replace a Redis server with that corrected file.
Replacing a Failed Master
Suppose machine A is running a copy of Redis that’s acting as the master
, and machine B is running a copy of Redis that’s acting as the slave
. Unfortunately, machine A has just lost network connectivity for some reason that we haven’t yet been able to diagnose. But we have machine C with Redis installed that we’d like to use as the new master
.
Note:
- If we cannot access machine A, then usually we cannot access and correct the
AOF
. However, if we could, then we could simply use the correctAOF
file we just fixed in the last section to become theAOF
file for Machine C. Then, we set machine B to be the slave of machine C, and we finish.
In this case, we can also tell machine B to produce a fresh snapshot with
SAVE
. We’ll then copy that snapshot over to machine C. After the snapshot has been copied into the proper path, we’ll start Redis on machine C. Finally, we’ll tell machine B to become aslave
of machine C.1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29user@vpn-master ~:$ ssh root@machine-b.vpn
Last login: Wed Mar 28 15:21:06 2012 from ...
Connect to machine B on our VPN network.
root@machine-b ~:$ redis-cli # Start up the commandline redis client to do a few simple operations.
redis 127.0.0.1:6379> SAVE
OK
redis 127.0.0.1:6379> QUIT # Start a SAVE, and when it’s done, QUIT so that we can continue.
root@machine-b ~:$ scp +
> /var/local/redis/dump.rdb machine-c.vpn:/var/local/redis/ # Copy the snapshot over to the new master, machine C.
dump.rdb 100% 525MB 8.1MB/s 01:05
root@machine-b ~:$ ssh machine-c.vpn
Last login: Tue Mar 27 12:42:31 2012 from ...
root@machine-c ~:$ sudo /etc/init.d/redis-server start
Starting Redis server...
Connect to the new master and start Redis.
root@machine-c ~:$ exit
root@machine-b ~:$ redis-cli
redis 127.0.0.1:6379> SLAVEOF machine-c.vpn 6379
OK
redis 127.0.0.1:6379> QUIT
root@machine-b ~:$ exit
user@vpn-master ~:$As an alternative to creating a new
master
, we may want to turn theslave
into amaster
and create a newslave
. Either way, Redis will be able to pick up where it left off, and our only job from then on is to update our client configuration to read and write to the proper servers, and optionally update the on-disk server configuration if we need to restart Redis.
REDIS SENTINEL
A relatively recent addition to the collection of tools available with Redis is Redis Sentinel
. By the final publishing of this manuscript, Redis Sentinel should be complete. Generally, Redis Sentinel pays attention to Redis masters and the slaves of the masters and automatically handles failover if the master goes down. For more details, please visit this link.
Redis Transactions
(This section builds on from the section Basic Redis Transactions)
Because of Redis’s delaying execution of commands until EXEC
is called when using MULTI
/EXEC
, many clients (including the Python client that we’re using) will hold off on even sending commands until all of them are known. When all of the commands are known, the client will send MULTI
, followed by the series of commands to be executed, and EXEC
, all at the same time. The client will then wait until all of the replies from all of the commands are received. This method of sending multiple commands at once and waiting for all of the replies is generally referred to as pipelining, and has the ability to improve Redis’s performance when executing multiple commands by reducing the number of network round trips that a client needs to wait for.
However, there could be a potential problem of error: since MULTI
holds the command back before executing, if some data has been changed (e.g. deleted) in the redis server, then there could be an error or data corruption when you execute your EXEC
.
Consider the example of a Fake Game Company has seen major growth in their webbased RPG that’s played on YouTwitFace, a fictional social network. Because it pays attention to the needs and desires of its community, it has determined that the players need the ability to buy and sell items in a marketplace. It’s our job to design and build a marketplace that can scale to the needs of the community.
Posting Items in a Marketplace
Our requirements for the market are simple: a user can list an item for a given price, and when another user purchases the item, the seller receives the money. We’ll also say that the part of the market we’ll be worrying about only needs to be ordered by selling price. In a later chapter, we’ll cover some topics for handling other orders.
Our basic marketplace that includes an ItemA being sold by user 4 for 35 e-dollars. To include enough information to sell a given item in the market, we’ll concatenate the item ID for the item with the user ID of the seller and use that as a member of a market ZSET, with the score being the item’s selling price. By including all of this information together, we greatly simplify our data structures and what we need to look up, and get the benefit of being able to easily paginate through a presorted market.
Now, we need to deal with the code part.
In the process of listing, we’ll use a Redis operation called WATCH
, which we combine with MULTI
and EXEC
, and sometimes UNWATCH
or DISCARD
. When we’ve watched keys with WATCH
, if at any time some other client replaces, updates, or deletes any keys that we’ve WATCH
ed before we have performed the EXEC
operation, our operations against Redis will fail with an error message when we try to EXEC
(at which point we can retry or abort the operation). By using WATCH
, MULTI
/EXEC
, and UNWATCH
/DISCARD
, we can ensure that the data that we’re working with doesn’t change while we’re doing something important, which protects us from data corruption.
Basically,
WATCH
works by making theEXEC
command conditional; in other words, Redis will only perform a transaction if theWATCH
ed keys were not modified (otherwise it returnsnil
no matter what commands you queued). If aWATCH
ed key was indeed modified, the transaction won’t be entered at all.The
WATCH
command can be called numerous times (before theMULTI
command), and oneWATCH
call can involve any number of keys.Watch
calls simply monitor for changes starting from the point whereWATCH
was called untilEXEC
is called. OnceEXEC
is invoked, all the keys will beUNWATCH
ed, whether or not the transaction in question was aborted. Closing a client connection also triggers all keys to beUNWATCH
ed.
Note:
- While
UNWATCH
is obvious as it sounds,DISCARD
needs more explaination. In the same way thatUNWATCH
will let us reset our connection if sent afterWATCH
but beforeMULTI
,DISCARD
will also reset the connection if sent afterMULTI
but beforeEXEC
. That is to say, if we’dWATCH
ed a key or keys, fetched some data, and then started a transaction withMULTI
followed by a group of commands, we could cancel theWATCH
and clear out any queued commands withDISCARD
. We don’t useDISCARD
here, primarily because we know whether we want to perform aMULTI
/EXEC
orUNWATCH
, so aDISCARD
is unnecessary for our purposes.
Therefore, all we need to do for listing/posting those items would be to add the item to the market ZSET
, while WATCH
ing the seller’s inventory to make sure that the item is still available to be sold.
1 | def list_item(conn, itemid, sellerid, price): |
Generally, listing an item should occur without any significant issue, since only the user should be selling their own items (which is enforced farther up the application stack). But as mentioned before, if a user’s inventory were to change between the WATCH
and EXEC
, our attempt to list the item would fail, and we’d retry.
Purchasing an Item
To process the purchase of an item, we need to:
- first
WATCH
the market and the user who’s buying the item. - then fetch the buyer’s total funds and the price of the item
- verify that the buyer has enough money. If they don’t have enough money, we cancel the transaction.
- if they do have enough money, we perform the transfer of money between the accounts, move the item into the buyer’s inventory, and remove the item from the market.
- on
WATCH
error, we retry for up to 10 seconds in total.
1 | def purchase_item(conn, buyerid, itemid, sellerid, lprice): |
Q: Why can’t the data be simply locked in Redis?
When accessing data for writing (SELECT FOR UPDATE
in SQL), relational databases will place a lock on rows that are accessed until a transaction is completed with COMMIT
or ROLLBACK
. If any other client attempts to access data for writing on any of the same rows, that client will be blocked until the first transaction is completed. This form of locking works well in practice (essentially all relational databases implement it), though it can result in long wait times for clients waiting to acquire locks on a number of rows if the lock holder is slow.
Because there’s potential for long wait times, and because the design of Redis minimizes wait time for clients (except in the case of blocking LIST
pops), Redis doesn’t lock data during WATCH
. Instead, Redis will notify clients if someone else modified the data first, which is called optimistic locking (the actual locking that relational databases perform could be viewed as pessimistic). Optimistic locking also works well in practice because clients are never waiting on the first holder of the lock; instead they retry if some other client was faster.
Non-transactional Pipeline
In the python redis libraray, you can actually put arguments in the pipeline()
function:
1 | pipe = conn.pipeline(True) # this is the same as conn.pipeline() |
And as we have seen before, this will wrap the commands with MULTI
/EXEC
, so that all commands will execute in order in a transactional way. However, we could also pass in the argument False
:
1 | pipe = conn.pipeline(False) |
This will not wrap the commands with MULTI
/EXEC
, but it will have an effect of executing multiple commands at once in the redis server, and thereby reduce the time taken for roundtrips. You can also think of these as comamnds with a multiple arguments, like MGET
, MSET
, HMGET
, HMSET
, RPUSH/LPUSH
, SADD
, ZADD
, and others (but the performance improvement is perhaps not as drastic as these commands).
However, this does means that it is appropriate only when we are sure the result of one command doesn’t affect the input to another, and we don’t need them all to execute transactionally. However, we are sure that it will reduce the amount of round trips between the client and the server (to one in the example below), so pipeline(False)
method can further improve overall Redis performance.
For example:
1 | def update_token(conn, token, user, item=None): |
could be replaced by:
1 | def update_token_pipeline(conn, token, user, item=None): |
Redis Performance and Benchmarking
Improving performance in Redis requires having an understanding of what to expect in terms of performance for the types of commands that we’re sending to Redis. To get a better idea of what to expect from Redis, we’ll quickly run a **benchmark that’s included within Redis: redis-benchmark
:
1 | root@a93d918c25f7:/data# redis-benchmark -c 1 -q |
The output of redis-benchmark
shows a group of commands that are typically used in Redis, as well as the number of commands of that type that can be run in a single second. A standard run of this benchmark without any options will try to push Redis to its limit using 50
clients, but it’s a lot easier to compare performance of a single benchmark client against one copy of our own client (by specifying the 1
option, as in the command show in the example above), rather than many.
However, be careful if you are using the python redis library. When looking at the output of redis-benchmark, we must be careful not to try to directly compare its output with how quickly our application (e.g. python) performs. This is because redis-benchmark
doesn’t actually process the result of the commands that it performs, which means that the results of some responses that require substantial parsing overhead aren’t taken into account. Generally, compared to redis-benchmark running with a single client, we can expect the Python Redis client to perform at roughly 50–60% of what redis-benchmark will tell us for a single client and for nonpipelined commands, depending on the complexity of the command to call.
Logging in Redis
In the world of Linux and Unix, there are two common logging methods. The first is logging to a file, where over time we write individual log lines to a file, and every once in a while, we write to a new file. Many thousands of pieces of software have been written do this (including Redis itself). But this method can run into issues because we have many different services writing to a variety of log files, each with a different way of rolling them over, and no common way of easily taking all of the log files and doing
something useful with them.
Running on TCP and UDP port 514 of almost every Unix and Linux server available is a service called syslog
, the second common logging method. Syslog accepts log messages from any program that sends it a message and routes those messages to various on-disk log files, handling rotation and deletion of old logs. With configuration, it can even forward messages to other servers for further processing. As a service, it’s far more convenient than logging to files directly, because all of the special log file rotation and deletion is already handled for us.
Recent Logs
To keep a recent list of logs, we’ll LPUSH
log messages to a LIST
and then trim that LIST
to a fixed size. Later, if we want to read the log messages, we can perform a simple LRANGE
to fetch the messages. We’ll take a few extra steps to support different named log message queues and to support the typical log severity levels, but you can remove either of those in your own code if you need to. The code for writing recent logs to Redis is shown below:
1 | SEVERITY = { |
Logging Common operations
The problem with the above code snippet is that it’s not very good at telling you whether any important messages were lost in the noise. By recording information about how often a particular message appears, you could then look through the messages ordered by how often they happened to help you determine what’s important.
1 | def log_common(conn, name, message, severity=logging.INFO, timeout=5): |
Locks in Redis
In the context of Redis, we’ve been using WATCH
as a replacement for a lock, and we call it optimistic locking, because rather than actually preventing others from modifying the data, we’re notified if someone else changes the data before we do it ourselves.
With distributed locking, we have the same sort of acquire, operate, release operations, but instead of having a lock that’s only known by threads within the same process, or processes on the same machine, we use a lock that different Redis clients on different machines can acquire and release. When and whether to use locks or WATCH
will depend on a given application; some applications don’t need locks to operate correctly, some only require locks for parts, and some require locks at every step.
Problems with Watch
Consider the market example that we solved before with WATCH
and UNWATCH
: we WATCH
the seller’s inventory to make sure the item is still available, add the item to the market ZSET
, and remove it from the user’s inventory; to purchase of an item, we WATCH
the market and the buyer’s HASH
.
This would work correctly, however, when the scale of operation increases, the amount of retries that happen becomes huge, which could affect the latency:
- | Listed items | Bought items | Purchase retries | Average wait per purchase |
---|---|---|---|---|
1 lister, 1 buyer | 145,000 | 27,000 | 80,000 | 14ms |
5 listers, 1 buyer | 331,000 | <200 | 50,000 | 150ms |
5 listers, 5 buyers | 206,000 | <600 | 161,000 | 498ms |
To get past this limitation and actually start performing sales at scale, we must make sure that we only list or sell one item in the marketplace at any one time. We do this by using a lock.
Building a Simple Lock
The first part of making sure that no other code can run is to acquire the lock. The natural building block to use for acquiring a lock is the SETNX
command, which will only set a value if the key doesn’t already exist. We’ll set the value to be a unique identifier to ensure that no other process can get the lock, and the unique identifier we’ll use is a 128-bit randomly generated UUID.
If we fail to acquire the lock initially, we’ll retry until we acquire the lock, or until a specified timeout has passed, whichever comes first, as shown here.
1 | def acquire_lock(conn, lockname, acquire_timeout=10): |
Now that we have the lock, we can perform our buying or selling without WATCH
errors getting in our way. We’ll acquire the lock and, just like before, check the price of the item, make sure that the buyer has enough money, and if so, transfer the money and item. When completed, we release the lock.
1 | def purchase_item_with_lock(conn, buyerid, itemid, sellerid): |
Between the time when we acquired the lock and when we’re trying to release it, someone may have done bad things to the lock. To release the lock, we need to WATCH
the lock key, and then check to make sure that the value is still the same as what we set it to before we delete it. This also prevents us from releasing a lock multiple times. The release_lock()
function is shown next.
1 | def release_lock(conn, lockname, identifier): |
Note:
- For simplicity, we performed lock on the entire
market
data, which also means that listing an item could be blocked. However, if we replace the market-level lock with one specific to the item to be bought or sold, we can reduce lock contention and increase performance.
Locks with timeouts
The above locks still had one important problem: it doesn’t handle cases where a lock holder crashes without releasing the lock, or when a lock holder fails and holds the lock forever. To handle the crash/failure cases, we add a timeout to the lock.
In order to give our lock a timeout, we’ll use EXPIRE
to have Redis time it out automatically.
The natural place to put the EXPIRE
is immediately after the lock is acquired, and we’ll do that. But if our client happens to crash (and the worst place for it to crash for us is between SETNX
and EXPIRE
), we still want the lock to eventually time out. To handle that situation, any time a client fails to get the lock, the client will check the expiration on the lock, and if it’s not set, set it. This will make sure that the lock will always have a timeout, and will eventually expire, letting other clients get a timed-out lock.
1 | def acquire_lock_with_timeout(conn, lockname, acquire_timeout=10, lock_timeout=10): |
This new acquire_lock_with_timeout()
handles timeouts. It ensures that locks expire as necessary, and that they won’t be stolen from clients that rightfully have them. Even better, we were smart with our release lock function earlier, which still works.
Note:
- As of Redis 2.6.12, *the
SET
command added options to support a combination ofSETNX
andSETEX
functionality, which makes our lock acquire function trivial. We still need the complicated release lock to be correct. In python, this looks like:set(name, value, ex=None, px=None, nx=False, xx=False, keepttl=False)
Building a Semaphore
Sometimes, we need to use a semaphore to hold the ocks, instead of having a timeout lock shown above for a single use.
When building a counting semaphore, we run into many of the same concerns we had with other types of locking. We must decide who got the lock, how to handle processes that crashed with the lock, and how to handle timeouts. If we don’t care about timeouts, or handling the case where semaphore holders can crash without releasing semaphores.
In almost every case where we want to deal with timeouts in Redis, we’ll generally look to one of two different methods. Either we’ll use EXPIRE
like we did with our standard locks, or we’ll use ZSETs
. In this case, we want to use ZSET
s, because that allows us to keep information about multiple semaphore holders in a single structure.
Now, to build a semaphore, we will have: for each process that attempts to acquire the semaphore, we’ll generate a unique identifier. This identifier will be the member of a ZSET
. For the score, we’ll use the timestamp for when the process attempted to acquire the semaphore. When a process wants to attempt to acquire a semaphore, it first generates an identifier, and then the process adds the identifier to the ZSET
using the current timestamp as the score. After adding the identifier, the process then checks for its identifier’s rank. If the rank returned is lower than the total allowed count (Redis uses 0-indexing on rank), then the caller has acquired the semaphore. Otherwise, the caller doesn’t have the semaphore and must delete its identifier from the ZSET
.
1 | def acquire_semaphore(conn, semname, limit, timeout=10): |
Releasing the semaphore is easy: we remove the identifier from the ZSET, as can be
seen in the next listing.
1 | def release_semaphore(conn, semname, identifier): |
However, there is a problem in this design as well. Since it is relying on every process having access to the same system time in order to get the semaphore can cause problems if we have multiple hosts. This isn’t a huge problem for our specific use case, but if we had two systems A and B, where A ran even 10 milliseconds faster than B, then if A got the last semaphore, and B tried to get a semaphore within 10 milliseconds, B would actually “steal” A’s semaphore without A knowing it.
Any time we have a lock or a semaphore where such a slight difference in the system clock can drastically affect who can get the lock, the lock or semaphore is considered unfair
. Unfair locks and semaphores can cause clients that should’ve gotten the lock or semaphore to never get it, and this is something that we’ll fix in the next section.
Fair Semaphore
In order to minimize problems with inconsistent system times, we’ll add a counter
(a String
) and a second ZSET
. The counter
creates a steadily increasing timer-like mechanism that ensures that whoever incremented the counter first should be the one to get the semaphore.
We then enforce our requirement that clients that want the semaphore who get the counter
first also get the semaphore inside an “owner” ZSET
with the counter-produced value as the score, checking our identifier’s rank in the new ZSET
to determine which client got the semaphore.
We continue to handle timeouts the same way as our basic semaphore, by removing entries from the system time ZSET
. We propagate those timeouts to the new owner ZSET
by the use of ZINTERSTORE
and the WEIGHTS
argument.
1 | def acquire_fair_semaphore(conn, semname, limit, timeout=10): |
Releasing the semaphore is almost as easy as before, only now we remove our identifier from both the owner and timeout ZSET
s, as can be seen in this next listing.
1 | def release_fair_semaphore(conn, semname, identifier): |