redis data types
Contents
Redis Data Type Summary
There are plenty of good resources on the internet, but I’m writing this up as study
Testing is done with redis-cli, but for real use I’d probably use an api library for the language
Note. Coming back to this about ten years later, a lot was missing. I re-ran every example and filled in the gaps. The output below is from Redis 7.0, so it may differ slightly from older versions.
The official docs to read are these two:
- Overview: http://redis.io/topics/data-types-intro
- Examples: http://redis.io/topics/data-types
Before the types themselves
A few things apply to every key regardless of type. Not knowing these causes confusion later.
One key, one type
A key holds exactly one type. Create it as a string and run a list command on it and you get an error right away.
| |
WRONGTYPE almost always means key names collided. That’s why the convention is
to namespace key names with colons, like user:1000:name.
type / exists / del
When you don’t know what type a key holds, use type.
| |
Expiry (expire / ttl)
The most important one if you’re using Redis as a cache. expire sets seconds,
ttl reads it back.
| |
ttl has three kinds of return value, which trips people up:
- positive: seconds remaining
-1: the key exists but has no expiry-2: the key doesn’t exist at all
| |
Also, expiry is per key, not per field. There’s no way to expire a single field of a hash 30 seconds from now. Deleting a field leaves the key’s TTL alone.
| |
scan, not keys
keys * is convenient, but Redis is single threaded, so with many keys the whole
server stalls while it runs. In production use scan and walk a cursor.
| |
The first value is the next cursor; 0 means you’ve been all the way around.
count is a hint rather than an exact size, so you can’t predict how many come back.
Strings
- Stores characters, numbers, etc. as values
- No separate type on save (no distinction between numbers and strings)
- Numbers can be stored too, and atomic counter operations like
incr,incrby,decr,decrbyare possible on them incrby,decrbyadd or subtract a specific number, used likeincrby "test_strings" 10
| |
Counters
Good enough for things like view counts. The incr family is atomic, so nothing
is lost when several processes hit it at once. The point is not having to read,
add one, and write back.
| |
For decimals there’s incrbyfloat. Trailing zeros get dropped.
| |
A non-numeric value is an error, naturally.
| |
Storing with expiry (sessions)
setex does set + expire in one shot. Useful for sessions or verification codes.
| |
nx as a rough lock
set key value nx ex 10 writes only if the key doesn’t exist and sets a
10 second expiry. If it’s already there you get (nil). Since that’s atomic it
works as a simple distributed lock.
| |
Releasing it with a plain del is wrong: your lock may have already expired and
you’d delete someone else’s. The usual approach is to store an identifier in the
value and compare-then-delete in Lua. For a proper implementation you want the
Redlock document.
Several at once
To cut round trips, mset / mget. Missing keys come back as (nil) in place.
| |
String manipulation
You can append to a value or read a slice of it. Handy for accumulating logs.
| |
Lists
- Stores a list as the value
- lrange: reads values, where -1 means fetch everything
| |
lpush pushes on the left (head), rpush on the right (tail). That’s why
pushing 1 then 2 with lpush puts 2 in front.
Length and positional access are available too. Negative indexes count from the end.
| |
Queues
- Seems like you could implement a queue with
rpop
| |
lpush in and rpop out gives a FIFO queue; lpush / lpop gives a stack.
Blocking with brpop
With brpop it seems you could even implement sequential distributed work.
Similar to rpop but if there’s no data it waits in a block state until data
comes in, so you don’t have to poll an empty list.
Run this in one terminal and it just sits there:
| |
Push from another terminal and it returns. You get the key it came from as well:
| |
| |
The last argument is a timeout in seconds; 0 waits forever. On timeout you get (nil).
| |
ltrim to keep only the last N
For things like “3 recently viewed items”. lpush then ltrim so the list
doesn’t grow without bound.
| |
ltrim takes the range to keep, not the range to remove. Easy to get backwards.
Removing values, inserting in the middle
lrem key count value removes up to count occurrences from the front. Zero means all.
| |
lmove for a queue that doesn’t drop work
If a worker dies after rpop, that job is simply gone. lmove pops and pushes
onto another list atomically, so the job stays in a processing list and can be
recovered.
| |
When the job finishes, lrem it from processing. The old command was
rpoplpush, whose direction was fixed, which is why lmove was added (6.2+).
The blocking version is blmove.
Sets
- Holds the value as a set
- Lists allow duplicates but sets don’t
| |
Note that sadd returns 3, not 4. It counts only the members actually added;
1 was already there.
Order isn’t guaranteed. It looking sorted above is a coincidence.
Cardinality, membership, removal
Don’t pull everything with smembers to count or search. There are dedicated
commands.
| |
sismember is O(1), which makes it a good fit for checks like “has this user
seen this”.
Set operations
This is the real reason to use a set. Intersection, union and difference are done server side.
| |
To store the result under a key, use sinterstore. It returns the number stored.
| |
If you only need the count, sintercard (7.0+) is cheaper because it never
materializes the result. With limit it stops counting there.
| |
Set operations get heavy with many members, so running them live across large sets is best avoided.
Random members
srandmember only reads; spop removes what it returns. Useful for draws.
| |
Hashes
- Hashes hold a key/value list as the value
| |
hgetall returns fields and values alternating. Client libraries fold that into
a map for you.
- hget returns (nil) if there’s no value
| |
- Changing the value for a hashkey
| |
That 0 isn’t a failure — it means the field already existed and was overwritten
rather than newly created.
Multiple fields at once
hset takes any number of field/value pairs (4.0+). The old hmset is deprecated.
| |
Per-field counters
hincrby increments a single field. Good for things like a login count.
| |
A missing field starts from 0, so there’s no need to initialize it.
hsetnx writes only when the field is absent.
| |
Why a hash instead of several strings
Instead of scattering user:1:name, user:1:city and so on as separate strings,
grouping them into one hash saves a fair amount of memory. Measuring five fields
directly:
| |
Same data, 344 vs 120. The per-key overhead disappears. At a million users that difference adds up.
Only cheap while small
With few fields a hash is stored as a listpack, a flat array. Past a threshold
it converts to a real hash table and memory jumps.
| |
Measuring 512 fields against 600:
| |
17% more fields, 5x the memory. If you’re thinking of packing thousands of fields
into one hash, splitting keys is probably better.
(Older versions called this encoding ziplist, with the setting
hash-max-ziplist-entries.)
Sorted sets
- Holds the value as a set
- Like
Sets, no duplicates - Stored together with a score, and sorted by score
- Seems usable like a list, with the advantage of being sorted
| |
- Don’t use a string for the score
| |
- Since duplicates aren’t allowed, inserting the same value overwrites the existing data’s score and the order changes
| |
Add withscores to see the scores too, which you need when checking how things
got ordered.
| |
The argument order is zadd key score member, score first. Coming from sadd
you’ll keep writing it backwards.
Leaderboards
The obvious use. Add scores, slice in reverse.
| |
zrange goes low to high, zrevrange high to low. For rank and score on their own:
| |
zrank is zero based, so add one before displaying it.
To add to a score, zincrby. Same idea as incrby on a string, except the
ordering follows automatically.
| |
Selecting by score range
Where zrange works on rank (index), zrangebyscore works on score. A leading
( makes the bound exclusive.
| |
Open the range with -inf / +inf, and page with limit.
| |
Deletion comes in both flavors: by score (zremrangebyscore) and by rank
(zremrangebyrank).
A timestamp as the score gives you a delayed queue
Use the score as a timestamp instead of a score and you can pull out “jobs due now”. Sorted sets get used for this as much as for rankings.
| |
Reading and removing are separate steps, so multiple workers can collide. Wrap it
in Lua or use zpopmin.
Keeping only the best score
A plain zadd always overwrites, which can lower a score. gt updates only when
the new score is greater (6.2+). ch makes the return value count changes rather
than insertions.
| |
nx writes only when absent, xx only when present.
| |
Bitmaps
- Stores bit values
- Can store 2^32 (4.2 billion) bit values in 512MB
- Seems good for storing boolean option values (like whether each member has viewed a notice)
| |
setbit returns the previous value. It isn’t really a separate type — it’s a
string viewed bit by bit.
Daily visits
Make a key per day and use the user id as the bit offset. bitcount gives that
day’s visitor count directly.
| |
Visitors on both days is bitop and; on either day is bitop or.
| |
The 51 that bitop returns is the byte length of the result string, not a count
of people. Count with bitcount separately.
The first set bit is bitpos.
| |
How much it actually saves
One day of visit flags for a million users:
| |
122KB. The same thing as a Set has to hold a million members, which runs to tens of MB. The tradeoff is that you can’t list who visited without walking the bits, and ids have to be dense integers for this to pay off. With UUIDs it doesn’t work.
HyperLogLog
The rest wasn’t in the original post, but it belongs here.
Use it when you only need a distinct count (cardinality). It doesn’t store the members; it estimates probabilistically and uses almost no memory. In exchange the number isn’t exact and you can’t read members back out.
| |
pfmerge combining daily counters into weekly or monthly UV is the nice part.
With a Set you’d have to keep all the originals around.
I put 100,000 members in and compared against a Set:
| |
The real count was 100,000 and it reported 100,420 — off by 0.42%. In return, 14KB against 4.6MB, a 330x memory difference. The documented standard error is 0.81%, so that’s about right.
Good for metrics like UV where “roughly how many” is enough. Don’t use it for billing or settlement.
Geo
Store coordinates and query distance or radius. Added in 3.2.
| |
Longitude comes first, latitude second. Map APIs usually do the reverse, which is confusing.
Radius search is geosearch (6.2+). The older georadius is deprecated.
| |
Notice the coordinates that come back differ slightly from what went in — geohash encoding loses precision. Not a fit if you need meter-level accuracy.
It isn’t a new type; underneath it’s a sorted set with a geohash as the score.
| |
Which is why you remove entries with zrem — there’s no geo-specific delete command.
Non-ASCII members are displayed escaped by redis-cli, like "\xed\x99\x8d...".
The data isn’t corrupted, only the display; redis-cli --raw shows it properly.
Streams
A log-shaped type added in 5.0, much later than the six above. A List used as a queue loses the item the moment you pop it; a Stream keeps entries like a log and lets several consumers each track their own position. Roughly Kafka shaped.
| |
* auto-generates the id. The milliseconds-sequence format means several
entries in the same millisecond still don’t collide. Each entry holds field/value
pairs like a hash.
Consumer groups
Workers in the same group split the messages. What a worker reads stays “pending”
until it calls xack. If a worker dies the entry is still pending, so another
worker can claim it.
| |
> means “entries nobody has claimed yet”. After xack the pending list empties.
| |
This is the main difference from a List queue: entries don’t vanish on read, they require an ack.
Capping growth
Being a log, it grows forever if you let it. maxlen sets an upper bound. With
~ it trims to approximately that many rather than exactly, which is much cheaper.
| |
Which type for what
| Type | Good for | Watch out |
|---|---|---|
| Strings | Cache, counters, sessions, simple locks | Large values move whole |
| Lists | Queues, recent-item lists | Middle access is O(N) |
| Sets | Tags, dedup, intersections | Set ops get heavy with many members |
| Hashes | Grouping one object | Memory jumps past thousands of fields |
| Sorted sets | Rankings, delayed queues, range queries | Scores are doubles, large integers lose precision |
| Bitmaps | Bulk booleans (visits, seen flags) | Only pays off with dense integer ids |
| HyperLogLog | Approximate distinct counts like UV | 0.81% error, members not retrievable |
| Geo | Radius search | Coordinate precision loss, really a zset |
| Streams | Event logs, queues needing ack | Grows forever without maxlen |
Roughly, reason about it in this order:
- One value: String
- Several fields as one unit: Hash
- Order matters: List. Rank or range matters: Sorted set
- Deduplication is the point: Set. Only the count, and approximate is fine: HyperLogLog
- One on/off flag per user: Bitmap
- Failures have to be recoverable: Stream, not List